Hits: 168
[Udemy] Functional programming in python
2. Example – A function, interactive, calculator
Procedural vs Functional programming style
- Procedural
- line-by-line execution
- heavy use of statements(for, if…)
- heavy use of expressions(1+1, 2*3…)
- long functions(anything more than one line)
- Functional
- little use of statements
- heavy use of expressions
- singleline functions
Example
Procedural
OPERATORS = '+', '-', '*', '/'
def p_main():
number1 = p_get_number()
operator = p_get_operator()
number2 = p_get_number()
def p_get_number():
while True:
s = input('Enter an integer: )
try:
return int(s)
except ValueError:
print('That is not an integer')
def p_get_operator():
while True:
s = input('Enter an operator (+, -, *, /)')
if s in OPERATORS:
return s
else:
print('That is not an operator')
def p_calculate(number1, operator, number2):
if operator == '+':
return number1 + number2
if operator == '-':
return number1 - number2
if operator == '*':
return number1 * number2
if operator == '/':
return number1 / number2
raise Exception('Invalid operator')
p_main()
Functional
OPERATORS = '+', '-', '*', '/'
def f_get_number():
return int(inpur('Enter an integer: ')) # no validation
def f_get_operator():
return input('Enter an operator: ') # no validation
def f_calculate(number1, operator, number2):
return number1 + number2 if operator == '+'
else number1 - number2 if operator == '-'
else number1 * number2 if operator == '*'
else number 1 / number2 if operator == '/'
else None
def f_main():
return f_calculate(
f_get_number(),
f_get_operator(),
f_get_number()
)
3. Pro – Stateless, referentially transparent functions produce the same result.
- state and statelessness
- 什麼是 state?
- Functions are not executed in a void.
- global variables, objects with properties… are states.
- can influence how functions work.
- 什麼是 stateless functions
- always return the same result.
- given the same arguments.
- regardless of the program’s state
- side effects
- referential transparency
- allows parallel execution
- functions can be tested separately
# Stateful example
class Speaker():
def __init__(self, name):
self._name = name
def speak(self, text):
print('[%s] %s' % (self._name, text))
john = Speaker('John')
john.speak('Hello world!') # [John] Hello world!
carlos = Speaker('Carlos')
carlos.speak('Foobar!') # [Carlos] Foobar!
# FP
def speak(speaker, text):
print('[%s] %s' % (speaker, text))
john = 'John'
speak(john, 'Hello world!') # [John] Hello world!
carlos = 'Carlos'
speak(carlos, 'Foobar!') # [Carlos] Foobar!
4. Pro – You cna prove that code is correct at least in theory.
- unit test
- functions are tested with different kinds of input.
- to see if functions behave well, but no guarantees.
- 透過
assert()
,設計許多 input argument ,測試回傳值是否符合預期
- provability
- referential transparency allows formal provability
- beatiful in theory, difficult in practice
5. Con – Complexity and overly deep recursion.
- What is recursion:
- function 自摳
- elegant but difficult to follow
- limited by python’s maximum recursion depth(1000 times)
# procedural approach
def p_factorial(n):
f = 1
for i in range(1, n+1):
f *= i
return f
print(p_factorial(0)) # = 1 by convention
print(p_factorial(2)) # = 1×2 = 2
print(p_factorial(4)) # = 1×2×3x4 = 24
# functional approach
def f_factorial(n):
return 1 if n == 0 else n*f_factorial(n-1)
print(f_factorial(0)) # = 1 by convention
print(f_factorial(2)) # = 1×2 = 2
print(f_factorial(4)) # = 1×2×3x4 = 24
6. Con – Functional programming can be unintuitive
- 比較 oop 與 fp 的寫法是否直覺: oop 通常較為直覺,而 fp 則較不直覺。
# OOP
class Voter:
def __init__(self, name):
self.name = name
self.voted_for = None
def vote(self, politician):
self.voted_for = politician
politician.votes += 1
def __str__(self):
return self.name
class Politician:
def __init__(self, name):
self.name = name
self.votes = 0
def __str__(self):
return self.name
macron = Politician('Macron')
jean = Voter('Jean')
jean.vote(macron)
print('%s voted for %s' % (jean, jean.voted_for)) # Jean voted for Macron
print('%s received %d vote(s)' % (macron, macron.votes)) # Macron received 1 vote(s)
# FP
def vote(voters, politicians, voter, politician):
voters[voter] = politician
if politician in politicians:
politicians[politician] += 1
else:
politicians[politician] = 1
return voters, politicians
def voted_for(voters, voter):
return '%s voted for %s' % (voter, voters.get(voter, None))
def votes(politicians, politician):
return '%s received %d vote(s)' % (politician, politicians.get(politician, 0))
voters, politicians = vote({}, {}, 'Jean', 'Macron')
print(voted_for(voters, 'Jean')) # Jean voted for Macron
print(votes(politicians, 'Macron')) # Macron received 1 vote(s)
print(politicians) # {'Macron': 1}
7. The difference between statements and expressions.
-
statements
- instructions
- if, for, def, class …
- can’t print the result
-
expressions
- evaluate to something
- can print the result
- function calls are expression
8. Diving into lambda expression
lambda
is a expression without a name.- convinent function that is easy to implement.
9. Understand ‘and’ / ‘or’
用 and/or 來流程控制
# and 的用途
## 傳回第一個是 False 的物件,若所有物件都是 True 則傳回最後一個物件
print('a' and 'b' and 'c') # c
print(1 and 0 and 'a') # 0
print(1 and 2) # 2
print('---------')
# or 的用途
## 傳回第一個是 True 的物件,若所有物件都是 False 則傳回最後一個物件
print('a' or 'b' or 'c') # a
print('' or '' or '') # ''
print('' or 2 or '') # 2
10. Diving into inline if expressions
if statements
vs if expressions
# if statements
def p_grade_description(gp):
if gp > 7:
return 'good'
if gp > 5:
return 'sufficient'
return 'insufficient'
# if expression
lambda gp: 'good' if gp > 7 else 'sufficient' if gp > 5 else 'insufficient'
## execute
(lambda gp: 'good' if gp > 7 else 'sufficient' if gp > 5 else 'insufficient')(5) # 'insufficient'
## 一些 lambda 的範例
# 一個參數
print((lambda x:x + 1)(3)) # (3+1) = 4
# 兩個參數
print((lambda x, b:x ** b)(5, 3)) # (5 ** 3) = 125
# 以 function 包起來 - 一個參數
def my_lambda(n):
return lambda b:b ** n
print(my_lambda(5)(2)) # (2 ** 5) = 32
print(my_lambda(2)(5)) # (5 ** 2) = 25
# 以 function 包起來 - 兩個參數
def my_lambda2(n):
return lambda b, c:(b+c) ** n
print(my_lambda2(5)(2, 3)) # (2+3) ** 5 = 3125
11. Higher order functions – functions as arguments and return values.
- functions as arguments
- nested functions
- functions as return values
- operator module
- decorators, decorators with arguments
higher order function
- is a function that operates on other functions
l_factorial = lambda n: 1 if n == 0 else n*l_factorial(n-1)
# procedural style
import time
t0 = time.time()
l_factorial(900)
t1 = time.time()
print('Took: %.5f s' % (t1-t0))
# semi-function style
def timer(fnc, arg):
t0 = time.time()
fnc(arg)
t1 = time.time()
return t1-t0
print('Took: %.5f s' % timer(l_factorial, 900))
# pure function style
l_timestamp = lambda fnc, arg: (time.time(), fnc(arg), time.time()) # aka def timer(fnc, arg), return tuple
l_diff = lambda t0, retval, t1: t1-t0
l_timer = lambda fnc, arg: l_diff(*l_timestamp(fnc, arg)) # l_diff 的 input 是三個參數,因此要把 l_timestamp unpacking
print('Took: %.5f s' % l_timer(l_factorial, 900))
12. Nesting a function in another function.
- variable scope
詳細說明請見 另外一篇文章
13. Returning a function from another function.
這邊示範,如何以四個步驟烘烤可頌
# 四個步驟的參數
preheat_oven = lambda: print('Preheating oven')
put_croissants_in = lambda: print('Putting croissants in')
wait_five_minutes = lambda: print('Waiting five minutes')
take_croissants_out = lambda: print('Take croissants out (and eat them!)')
## 傳統解法
preheat_oven()
put_croissants_in()
wait_five_minutes()
take_croissants_out()
## 把四個步驟的函數丟到一個函數中
def perform_steps(*functions):
for function in functions:
function()
perform_steps(preheat_oven,
put_croissants_in,
wait_five_minutes,
take_croissants_out)
## 把四個步驟丟到函數後,再用一個物件(此例為 recipe )把函數存起來,等到要用的時候再執行 recipe()。
def create_recipe(*functions):
def run_all():
for function in functions:
function()
return run_all
recipe = create_recipe(preheat_oven,
put_croissants_in,
wait_five_minutes,
take_croissants_out)
recipe()
14. The perator module – operators as regular functions
- 因為 operator (
>, <, =, +, -, * ...
)不是物件,因此不能用於物件操作,因此可使用operator
module
這邊先用先前課堂中使用過的 l_factorial
示範
l_factorial = lambda n: 1 if n == 0 else n*l_factorial(n-1)
產生 chain_mul 函數,把 l_factorial 以及 l_factorial 的參數 n ,一起丟入 chain_mul 中,也就是 chain_mul((2*1) * (3*2))
,因此回傳 12
def chain_mul(*what):
total = 1
for (fnc, arg) in what:
total *= fnc(arg)
return total
chain_mul((l_factorial, 2), (l_factorial, 3)) # 12
但是上述的作法中,只能執行乘法的運算,因此利用 operator
這個套件,把 operator 寫的更靈活。
import operator
def chain(how, *what):
total = 1
for (fnc, arg) in what:
total = how(total, fnc(arg))
return total
chain(operator.mul, (l_factorial, 2), (l_factorial, 3)) # 12
15. Decorators – the @ prefix
- wrapping a function around a function
- allows you to add functionality to a function
- it is elegantly supported in python
繼續使用先前的 l_factorial function
def factorial(n):
return 1 if n == 0 else n*factorial(n-1)
我現在想知道,執行 factorial 這個函數,所需要的時間,傳統的作法是,透過新增一個函數 timer
,並把 factorial 這個函數作為參數丟到 timer
中執行。
import time
def timer(fnc):
def inner(arg):
t0 = time.time()
fnc(arg)
t1 = time.time()
return t1-t0
return inner
timed_factorial = timer(factorial)
timed_factorial(500)
但是這樣寫太冗長了,我們只要使用 @
這個 decorator ,就可以漂亮的完成這個任務。
@timer # decorator
def timed_factorial(n):
return 1 if n == 0 else n*factorial(n-1)
timed_factorial(500)
可以把這段文字想像成,我新增的這段 time_factorial 函數,要作為參數丟到 timer 函數中,也就是 timer(timed_factorial(n))
個概念。
16. Decorators with arguments
- 這張的目的是,把上一章提到的 decorators ,再新增一個時間單位,讓回傳的值可依據我的 decorator 決定回傳的時間單位。
- A decorator with arguments
- is a function that returns a decorator
- which in turn returns a function
- this requires nesting within nesting
- or three levels
老樣子,同樣的 factorial function。
def factorial(n):
return 1 if n == 0 else n*factorial(n-1)
為了能夠處理時間單位,新增一層 nested function:
import time
def timer_with_arguments(units='s'): # 第三層
def timer(fnc): # 第二層
def inner(arg): # 最內層
t0 = time.time()
fnc(arg)
t1 = time.time()
diff = t1-t0
if units == 'ms':
diff *= 1000
return diff # 最內層
return inner # 第二層
return timer # 第三層
timed_factorial = timer_with_arguments(units='ms')(factorial) # hard to read
timed_factorial(500)
這邊同樣使用 decorator 來完成,只要一行就搞定了
@timer_with_arguments(units='s')
def factorial(n)
return 1 if n == 0 else n*factorial(n-1)
factorial(100)
Design pattern of functional programing
17. Currying – one argument per function
-
what a design pattern is
- a standard solution to a common problem
-
how can you bind function arguments
-
how function arguments relates currying
- reduce a function with multiple arguments
- to a chain of higher-order functions that take
one argument
什麼是 currying(柯里化)?
- 每個函數都只有一個參數
- add(1,2,3) -> add(1)(2))(3)
- binding function arguments
def add(a, b, c):
return a + b + c
print(add(10,100,1000)) # 1100
from functools import partial # 把 function 參數固定下來
add_10 = partial(add, 10)
add_10_100 = partial(add_10, 100)
print(add_10_100(1000)) # 1100
from inspect import signature
def curry(fnc):
def inner(arg):
if len(signature(fnc).parameters) == 1:
return fnc(arg)
return curry(partial(fnc, arg))
return inner
@curry
def add(a, b, c):
return a + b + c
print(add(10)(100)(1000)) # 1100
Comments