標籤:
Python 進階 之 函數式編程寫在前面
入門階段的系列筆記傳送門 → 進這裡
已經掌握了基礎的內容如下:
- 變數和資料類型:Python 內建的基本類型
- List 和 Tuple:順序的集合類型
- 條件判斷和迴圈:控製程序流程
- Dict 和 Set:根據Key訪問的集合類型
- 函數:定義和調用函數
- 切片:如何對 list 進行切片
- 迭代:如何用 for 迴圈迭代集合類型
- 列表產生式:如何快速產生列表
接下來我要學會:
- 函數式編程
- 如何使用 Python 的模組(內建模組和第三方模組)
- 物件導向編程:物件導向的概念、屬性、方法、繼承、多態等
- 定製類:利用 Python 的特殊方法定製類
核心學習目標:
- 掌握函數式編程
- 掌握物件導向編程
- 能夠編寫模組化的程式
本次筆記的內容如下:
1. 函數式編程
函數式(functional)是一種編程的範式。
[ 不同語言的抽象層次不同:]
(高) Python → 函數式 計算 ↑ c語言 → 函數 ↑ ↑ 組合語言 ↑(低) 電腦硬體 → 指令 電腦
[ 函數式編程的特點:]
- 把計算視為函數而非指令
- 純函數式編程:不需要變數,沒有副作用,測試簡單
- 支援高階函數,代碼簡潔
[ Python 支援的函數式編程:]
- 不是純函數式編程:因為 Python 允許有變數
- 支援高階函數:函數可以作為變數傳入
- 支援閉包:有了閉包就能夠返回函數
- 有限度地支援匿名函數
1-1. 高階函數
變數可以指向函數,函數名其實就是指向函數的變數。
例如:add(-5,9,abs)
由於參數 x, y 和 f 都可以任意傳入,如果 f 傳入其他函數,就可以得到不同的傳回值。比如:add(25, 9, math.sqrt)
[ map() 函數:]
map() 是 Python 內建的高階函數,它接收一個函數 f 和一個 list,並通過把函數 f 依次作用在 list 的每個元素上,得到一個新的 list 並返回。
例如,對於 list [1, 2, 3, 4, 5, 6, 7, 8, 9],如果希望把 list 的每個元素都作平方,就可以用 map() 函數:
因此,我們只需要傳入函數 f(x)=x*x,就可以利用 map() 函數完成這個計算:
def f(x): return x*xprint map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
輸出結果:[1, 4, 9, 10, 25, 36, 49, 64, 81]
注意:map() 函數不改變原有的 list,而是返回一個新的 list。
利用 map() 函數,可以把一個 list 轉換為另一個 list,只需要傳入轉換函式。
由於 list 包含的元素可以是任何類型,因此,map() 不僅僅可以處理只包含數值的 list,事實上它可以處理包含任意類型的 list,只要傳入的函數 f 可以處理這種資料類型。
例子:假設使用者輸入的英文名字不規範,沒有按照首字母大寫,後續字母小寫規則,利用 map() 函數,把一個 list(包含若干不規範的英文名字)變成一個包含規範英文名字的 list
def format_name(s): return s[0].upper() + s[1:].lower()print map(format_name, [‘adam‘, ‘LISA‘, ‘barT‘])
[ reduce() 函數:]
reduce() 函數也是 Python 內建的一個高階函數。reduce() 函數接收的參數和 map() 類似,一個函數 f,一個 list,但行為和 map() 不同,reduce() 傳入的函數 f 必須接收兩個參數,reduce() 對 list 的每個元素反覆調用函數 f,並返回最終結果值。
例如,編寫一個 f 函數,接收 x 和 y,返回 x 和 y 的和:
def f(x, y): return x + y
調用 reduce(f, [1, 3, 5, 7, 9]) 時,reduce 函數將做如下計算:
- 先計算頭兩個元素:f(1, 3),結果為 4;
- 再把結果和第3個元素計算:f(4, 5),結果為 9;
- 再把結果和第4個元素計算:f(9, 7),結果為 16;
- 再把結果和第5個元素計算:f(16, 9),結果為 25;
- 由於沒有更多的元素了,計算結束,返回結果 25。
上述計算實際上是對 list 的所有元素求和。雖然 Python 內建了求和函數 sum(),但是,利用 reduce() 求和也很簡單。
reduce() 還可以接收第3個選擇性參數,作為計算的初始值。如果把初始值設為100,計算:
reduce(f, [1, 3, 5, 7, 9], 100)
結果將變為 125,因為第一輪計算是:
計算初始值和第一個元素:f(100, 1),結果為 101。
例子:Python內建了求和函數sum(),但沒有求積的函數,請利用recude()來求積
def prod(x, y): return x*yprint reduce(prod, [2, 4, 5, 7, 12])
[ filter()函數:]
filter() 函數是 Python 內建的另一個有用的高階函數,filter() 函數接收一個函數 f 和一個 list,這個函數 f 的作用是對每個元素進行判斷,返回 True 或 False,filter() 根據判斷結果自動過濾掉不合格元素,返回由符合條件元素組成的新 list。
例如,要從一個 list [1, 4, 6, 7, 9, 12, 17] 中刪除偶數,保留奇數,首先,要編寫一個判斷奇數的函數:
def is_odd(x): return x % 2 == 1
然後,利用 filter() 過濾掉偶數:
filter(is_odd, [1, 4, 6, 7, 9, 12, 17])# 結果:[1, 7, 9, 17]
利用 filter(),可以完成很多有用的功能,例如,刪除 None 或者Null 字元串:
def is_not_empty(s): return s and len(s.strip()) > 0filter(is_not_empty, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])# 結果:[‘test‘, ‘str‘, ‘END‘]
注意: s.strip(rm) 刪除 s 字串中開頭、結尾處的 rm 序列的字元。當 rm 為空白時,預設刪除空白符(包括’\n’, ‘\r’, ‘\t’, ’ ‘),如下:
a = ‘ 123‘a.strip()# 結果:‘123‘a=‘\t\t123\r\n‘a.strip()# 結果:‘123‘
例子:請利用 filter() 過濾出1~100中平方根是整數的數,即結果應該是 [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
import mathdef is_sqr(x): r = int(math.sqrt(x)) return r*r == xprint filter(is_sqr, range(1, 101))
[ 自訂排序函數:]
Python 內建的 sorted() 函數可對 list 進行排序:
>>> sorted([36, 5, 12, 9, 21])[5, 9, 12, 21, 36]
但 sorted() 也是一個高階函數,它可以接收一個比較函數來實現自訂排序,比較函數的定義是,傳入兩個待比較的元素 x, y,如果 x 應該排在 y 的前面,返回 -1,如果 x 應該排在 y 的後面,返回 1。如果 x 和 y 相等,返回 0。(-1 換,1 不換)
因此,如果我們要實現倒序排序,只需要編寫一個 reversed_cmp 函數:
def reversed_cmp(x, y): if x > y: return -1 if x < y: return 1 return 0
這樣,調用 sorted() 並傳入 reversed_cmp 就可以實現倒序排序:
>>> sorted([36, 5, 12, 9, 21], reversed_cmp)[36, 21, 12, 9, 5]
sorted() 也可以對字串進行排序,字串預設按照 ASCII 大小來比較:
>>> sorted([‘bob‘, ‘about‘, ‘Zoo‘, ‘Credit‘])[‘Credit‘, ‘Zoo‘, ‘about‘, ‘bob‘]
‘Zoo’ 排在 ‘about’ 之前是因為 ‘Z’ 的 ASCII 碼比 ‘a’ 小。
例子:對字串排序時,有時候忽略大小寫排序更符合習慣。利用 sorted() 高階函數,實現忽略大小寫排序的演算法
def cmp_ignore_case(s1, s2): s1 = s1.lower() s2 = s2.lower() if s1 < s2: return -1 if s1 > s2: return 1 return 0print sorted([‘bob‘, ‘about‘, ‘Zoo‘, ‘Credit‘], cmp_ignore_case)
[ 返回函數:]
Python 的函數不但可以返回 int、str、list、dict 等資料類型,還可以返回函數!
例如,定義一個函數 f(),我們讓它返回一個函數 g,可以這樣寫:
def f(): print ‘call f()...‘ # 定義函數g: def g(): print ‘call g()...‘ # 返回函數g: return g
仔細觀察上面的函數定義,我們在函數 f 內部又定義了一個函數 g。由於函數 g 也是一個對象,函數名 g 就是指向函數 g 的變數,所以,最外層函數 f 可以返回變數 g,也就是函數 g 本身。
調用函數 f,我們會得到 f 返回的一個函數:
>>> x = f() # 調用f()call f()...>>> x # 變數x是f()返回的函數:<function g at 0x1037bf320>>>> x() # x指向函數,因此可以調用call g()... # 調用x()就是執行g()函數定義的代碼
請注意區分返回函數和傳回值:
def myabs(): return abs # 返回函數def myabs2(x): return abs(x) # 返回函數調用的結果,傳回值是一個數值
返回函數可以把一些計算順延強制。例如,如果定義一個普通的求和函數:
def calc_sum(lst): return sum(lst)
調用calc_sum()函數時,將立刻計算並得到結果:
>>> calc_sum([1, 2, 3, 4])10
但是,如果返回一個函數,就可以“延遲計算”:
def calc_sum(lst): def lazy_sum(): return sum(lst) return lazy_sum
調用 calc_sum() 並沒有計算出結果,而是返回函數:
>>> f = calc_sum([1, 2, 3, 4])>>> f<function lazy_sum at 0x1037bfaa0>
對返回的函數進行調用時,才計算出結果:
>>> f()10
由於可以返回函數,我們在後續代碼裡就可以決定到底要不要調用該函數。
例子:編寫一個函數 calc_prod(lst),它接收一個 list,返回一個函數,返回函數可以計算參數的乘積
def calc_prod(lst): def g(): def f(x, y): return x * y return reduce(f,lst,1) return gf = calc_prod([1, 2, 3, 4])print f()
[ 閉包:]
在函數內部定義的函數和外部定義的函數是一樣的,只是他們無法被外部存取:
def g(): print ‘g()...‘def f(): print ‘f()...‘ return g
將 g 的定義移入函數 f 內部,防止其他代碼調用 g:
def f(): print ‘f()...‘ def g(): print ‘g()...‘ return g
但是,考察上一小節定義的 calc_sum 函數:
def calc_sum(lst): def lazy_sum(): return sum(lst) return lazy_sum
注意: 發現沒法把 lazy_sum 移到 calc_sum 的外部,因為它引用了 calc_sum 的參數 lst。
像這種內層函數引用了外層函數的變數(參數也算變數),然後返回內層函數的情況,稱為閉包(Closure)。
閉包的特點是返回的函數還引用了外層函數的局部變數,所以,要正確使用閉包,就要確保引用的局部變數在函數返回後不能變。舉例如下:
# 希望一次返回3個函數,分別計算1x1,2x2,3x3:def count(): fs = [] for i in range(1, 4): def f(): return i*i fs.append(f) return fsf1, f2, f3 = count()
原認為調用 f1(),f2() 和 f3()結果應該是 1,4,9,但實際結果全部都是 9。
原因就是當 count() 函數返回了3個函數時,這 3個函數所引用的變數 i 的值已經變成了 3。由於 f1、f2、f3 並沒有被調用,所以,此時他們並未計算 i*i,當 f1 被調用時:
>>> f1()9 # 因為 f1 現在才計算 i*i,但現在 i 的值已經變為 3
因此,返回函數不要引用任何迴圈變數,或者後續會發生變化的變數。
例子: 返回閉包不能引用迴圈變數,請改寫 count() 函數,讓它正確返回能計算 1x1、2x2、3x3 的函數。
考察下面的函數 f:
def f(j): def g(): return j*j return g
它可以正確地返回一個閉包 g,g 所引用的變數 j 不是迴圈變數,因此將正常執行。
在 count 函數的迴圈內部,如果藉助f函數,就可以避免引用迴圈變數 i。
答案:
def count(): fs = [] for i in range(1, 4): def f(j): def g(): return j*j return g r = f(i) fs.append(r) return fsf1, f2, f3 = count()print f1(), f2(), f3()
[ 匿名函數:]
高階函數可以接收函數做參數,有些時候,我們不需要顯式地定義函數,直接傳入匿名函數更方便。
在 Python中,對匿名函數提供了有限支援。還是以 map() 函數為例,計算 f(x)=x2 時,除了定義一個 f(x) 的函數外,還可以直接傳入匿名函數:
>>> map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])[1, 4, 9, 16, 25, 36, 49, 64, 81]
通過對比可以看出,匿名函數 lambda x: x * x 實際上就是:
def f(x): return x * x
關鍵字 lambda 表示匿名函數,冒號前面的 x 表示函數參數。
匿名函數有個限制,就是只能有一個運算式,不寫 return,傳回值就是該運算式的結果。
使用匿名函數,可以不必定義函數名,直接建立一個函數對象,很多時候可以簡化代碼:
>>> sorted([1, 3, 9, 5, 0], lambda x,y: -cmp(x,y))[9, 5, 3, 1, 0]
返回函數的時候,也可以返回匿名函數:
>>> myabs = lambda x: -x if x < 0 else x >>> myabs(-1)1>>> myabs(1)1
例子:利用匿名函數簡化以下代碼:
def is_not_empty(s): return s and len(s.strip()) > 0filter(is_not_empty, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])
答案:
def is_not_empty(s): return s and len(s.strip()) > 0print filter(lambda s : s and len(s.strip()) > 0, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])
1-2. decorator 裝飾器
問題:
- 定義了一個函數
- 想在運行時動態增加功能
- 又不想改動函數本身的代碼
樣本:
希望對下列函數調用增加 log 功能,列印函數調用:
def f1(x): return x*2def f2(): return x*xdef f3(): return x*x*x
方法 1:直接修改原函數
def f1(x): print ‘call f1()‘ return x*2def f2(): print ‘call f2()‘ return x*xdef f3(): print ‘call f3()‘ return x*x*x
有沒有更簡單的方法呢?
回顧一下高階函數:
是否可以接收一個函數,對其封裝,然後返回一個函數?
方法 2:通過高階函數返回新函數
def f1(x): return x*2def new_fn(f): # 裝飾器函數 def fn(x): print ‘call‘ + f._name_ + ‘()‘ return f(x) return fn
g1 = new_fn(f1)
print g1(5)
f1 = new_fn(f1)
print f1(5)
f1 的原始定義函數被徹底隱藏了,利用 f1 只能拿到裝飾以後的函數
[ 裝飾器:]
Python 內建的 @ 文法就是為了簡化裝飾器調用
@new_fndef f1(x): return x*2
==>
def f1(x): return x*2f1 = new_fn(f1)
[ 裝飾器的作用:]
可以極大地簡化代碼,避免每個函數編寫重複性代碼
- 列印日誌:@log
- 檢測效能:@performance
- 資料庫事務:@transaction
- URL路由:@post(‘/register’)
[ 編寫無參數 decorator:]
Python的 decorator 本質上就是一個高階函數,它接收一個函數作為參數,然後,返回一個新函數。
使用 decorator 用 Python 提供的 @ 文法,這樣可以避免手動編寫 f = decorate(f) 這樣的代碼。
考察一個 @log 的定義:
def log(f): def fn(x): print ‘call ‘ + f.__name__ + ‘()...‘ return f(x) return fn
對於階乘函數,@log 工作得很好:
@logdef factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))print factorial(10)
結果:
call factorial()…
3628800
但是,對於參數不是一個的函數,調用將報錯:
@logdef add(x, y): return x + yprint add(1, 2)
結果:
Traceback (most recent call last): File "test.py", line 15, in <module> print add(1,2)TypeError: fn() takes exactly 1 argument (2 given)
因為 add() 函數需要傳入兩個參數,但是 @log 寫死了只含一個參數的返回函數。
要讓 @log 自適應任何參數定義的函數,可以利用 Python 的 *args 和 **kw,保證任意個數的參數總是能正常調用:
def log(f): def fn(*args, **kw): print ‘call ‘ + f.__name__ + ‘()...‘ return f(*args, **kw) return fn
現在,對於任意函數,@log 都能正常工作。
例子:編寫一個 @performance ,它可以列印出函數調用的時間。
答案:
import timedef performance(f): def fn(*args, **kw): t1 = time.time() r = f(*args, **kw) t2 = time.time() print ‘call %s() in %fs‘ % (f.__name__, (t2 - t1)) return r return fn@performancedef factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))print factorial(10)
[ 編寫帶參數 decorator:]
考察上一節的 @log 裝飾器:
def log(f): def fn(x): print ‘call ‘ + f.__name__ + ‘()...‘ return f(x) return fn
發現對於被裝飾的函數,log 列印的語句是不能變的(除了函數名)。
如果有的函數非常重要,希望列印出 ‘[INFO] call xxx()…’,有的函數不太重要,希望列印出 ‘[DEBUG] call xxx()…’,這時,log 函數本身就需要傳入 ‘INFO’ 或 ‘DEBUG’ 這樣的參數,類似這樣:
@log(‘DEBUG‘)def my_func(): pass
把上面的定義翻譯成高階函數的調用,就是:
my_func = log(‘DEBUG‘)(my_func)
上面的語句看上去還是比較繞,再展開一下:
log_decorator = log(‘DEBUG‘)my_func = log_decorator(my_func)
上面的語句又相當於:
log_decorator = log(‘DEBUG‘)@log_decoratordef my_func(): pass
所以,帶參數的 log 函數首先返回一個 decorator 函數,再讓這個 decorator 函數接收 my_func 並返回新函數:
def log(prefix): def log_decorator(f): def wrapper(*args, **kw): print ‘[%s] %s()...‘ % (prefix, f.__name__) return f(*args, **kw) return wrapper return log_decorator@log(‘DEBUG‘)def test(): passprint test()
執行結果:
[DEBUG] test()…
None
對於這種3層嵌套的 decorator 定義,你可以先把它拆開:
# 標準decorator:def log_decorator(f): def wrapper(*args, **kw): print ‘[%s] %s()...‘ % (prefix, f.__name__) return f(*args, **kw) return wrapperreturn log_decorator# 返回decorator:def log(prefix): return log_decorator(f)
拆開以後會發現,調用會失敗,因為在 3 層嵌套的 decorator 定義中,最內層的 wrapper 引用了最外層的參數 prefix,所以,把一個閉包拆成普通的函數調用會比較困難。不支援閉包的程式設計語言要實現同樣的功能就需要更多的代碼。
例子:前面例子的 @performance 只能列印秒,給 @performace 增加一個參數,允許傳入’s’或’ms’
@performance(‘ms‘)def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))
答案:
import timedef performance(unit): def perf_decorator(f): def wrapper(*args, **kw): t1 = time.time() r = f(*args, **kw) t2 = time.time() t = (t2 - t1) * 1000 if unit == ‘ms‘ else (t2 - t1) print ‘call %s() in %f %s‘ % (f.__name__, t, unit) return r return wrapper return perf_decorator@performance(‘ms‘)def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))print factorial(10)# 結果: call factorial() in 7.776022 ms 3628800
[ 完善decorator:]
@decorator 可以動態實現函數功能的增加,但是,經過 @decorator “改造” 後的函數,和原函數相比,除了功能多一點外,有沒有其它不同的地方?
在沒有 decorator 的情況下,列印函數名:
def f1(x): passprint f1.__name__# 輸出: f1
有decorator的情況下,再列印函數名:
def log(f): def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) return wrapper@logdef f2(x): passprint f2.__name__# 輸出: wrapper
可見,由於 decorator 返回的新函數函數名已經不是 ‘f2’,而是 @log 內部定義的 ‘wrapper’。這對於那些依賴函數名的代碼就會失效。decorator 還改變了函數的 doc 等其它屬性。如果要讓調用者看不出一個函數經過了 @decorator 的 “改造”,就需要把原函數的一些屬性複製到新函數中:
def log(f): def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) wrapper.__name__ = f.__name__ wrapper.__doc__ = f.__doc__ return wrapper
這樣寫 decorator 很不方便,因為我們也很難把原函數的所有必要屬性都一個一個複製到新函數上,所以 Python 內建的 functools 可以用來自動化完成這個 “複製” 的任務:
import functoolsdef log(f): @functools.wraps(f) def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) return wrapper
最後需要指出,由於我們把原函數簽名改成了(*args, **kw),因此,無法獲得原函數的原始參數資訊。即便我們採用固定參數來裝飾只有一個參數的函數:
def log(f): @functools.wraps(f) def wrapper(x): print ‘call...‘ return f(x) return wrapper
也可能改變原函數的參數名,因為新函數的參數名始終是 ‘x’,原函數定義的參數名不一定叫 ‘x’。
例子:
import time, functoolsdef performance(unit): def perf_decorator(f): @functools.wraps(f) def wrapper(*args, **kw): t1 = time.time() r = f(* args, **kw) t2 = time.time() t = (t2 - t1) * 1000 if unit == ‘ms‘ else (t2-t1) print ‘call %s() in %f %s‘ % (f.__name__,t,unit) return r return wrapper return perf_decorator@performance(‘ms‘)def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))print factorial.__name__# 輸出:factorial
偏函數
當一個函數有很多參數時,調用者就需要提供多個參數。如果減少參數個數,就可以簡化調用者的負擔。
比如,int() 函數可以把字串轉換為整數,當僅傳入字串時,int() 函數預設按十進位轉換:
>>> int(‘12345‘)12345
但 int() 函數還提供額外的 base 參數,預設值為 10。如果傳入 base 參數,就可以做 N 進位的轉換:
>>> int(‘12345‘, base=8)5349>>> int(‘12345‘, 16)74565
假設要轉換大量的二進位字串,每次都傳入 int(x, base=2) 非常麻煩,於是,我們想到,可以定義一個 int2() 的函數,預設把 base=2 傳進去:
def int2(x, base=2): return int(x, base)
這樣,我們轉換二進位就非常方便了:
>>> int2(‘1000000‘)64>>> int2(‘1010101‘)85
functools.partial 就是協助我們建立一個偏函數的,不需要我們自己定義 int2(),可以直接使用下面的代碼建立一個新的函數 int2:
>>> import functools>>> int2 = functools.partial(int, base=2)>>> int2(‘1000000‘)64>>> int2(‘1010101‘)85
所以,functools.partial 可以把一個參數多的函數變成一個參數少的新函數,少的參數需要在建立時指定預設值,這樣,新函數調用的難度就降低了。
學習自 慕課網學習筆記 By 胡飛at 2016/4/10 13:04:49
一、Python 進階 之 函數式編程