[Udemy] Functional programming in python

點閱: 127

[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.

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

18. Monads – variables that decide how they should be treated

19. Memorization – remembering results

20. You cannot catch execptions in lambda expressions

21. Handling errors in lambda expressions

22. Example – a fully functional, interactive calculator

About the Author

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts