python:函式的高階特性
很多語言中,都允許把函式本身做為引數,傳遞給其它引數:即所謂的高階函式。python中也有類似特性:
一、map/reduce、filter、sorted
hadoop裡的map-reduce思想在python裡已經變成內建函數了。map是將某個函式逐一作用於列表中的每個元素。reduce則先從列表中取頭2個元素,傳到指定函式,然後將計算結果與餘下元素依次重複,直到List處理完。 1.1 map示例:(將List中的所有元素*10)def fn_map(x): print("fn_map->", x) return 10 * x L = [3, 4, 6, 8] print(list(map(fn_map, L))) print("\n")
輸出:
fn_map-> 3 fn_map-> 4 fn_map-> 6 fn_map-> 8 [30, 40, 60, 80]
結合map,我們再把reduce函式加上(最終效果:將所有元素*10再平方,最終得出 “平方和”的"平方根")
def fn_sqrt(x, y): print("fn_sqrt->", x, ",", y) return math.sqrt(x ** 2 + y ** 2) def fn_map(x): print("fn_map->", x) return 10 * x L = [3, 4, 6, 8] result = reduce(fn_sqrt, map(fn_map, L)) print(result) print("\n") print(math.sqrt((3 * 10) ** 2 + (4 * 10) ** 2 + (6 * 10) ** 2 + (8 * 10) ** 2))
注:要先import math,上面的程式碼輸出如下:
fn_map-> 3 fn_map-> 4 fn_sqrt-> 30 , 40 fn_map-> 6 fn_sqrt-> 50.0 , 60 fn_map-> 8 fn_sqrt-> 78.10249675906654 , 80 111.80339887498948 111.80339887498948
上面這個例子,可能實用性不大,下面給個實用性更強的示例,將每個單詞的首字母大寫,其它字母變小寫。
def normalize(name): return name[:1].upper() + name[1:].lower() L1 = ['adam', 'LISA', 'barT'] print(list(map(normalize, L1)))
輸出:
['Adam', 'Lisa', 'Bart']1.2 filter
filter跟java8裡的stream的filter是類似的,可以實現對集合中的元素,按某種規則進行篩選。
示例1:找出10以內的偶數
result = filter(lambda x: x % 2 == 0, range(1, 11)) print(list(result)) # 上面的寫法,等效於下面這個 def even(x): return x % 2 == 0 print(list(filter(even, range(1, 11))))
輸出:
[2, 4, 6, 8, 10] [2, 4, 6, 8, 10]
示例2:找出200以內的"回數"(即:從左向右,從右向左,都是一樣的數,比如:131, 141)
def is_palindrome1(n): if n < 10: return True s = str(n) for i in range(0, int(len(s) / 2)): if s[i] == s[-i - 1]: return True return False def is_palindrome2(n): s1 = str(n) s2 = list(reversed(s1)) return list(s1) == s2 print(list(filter(is_palindrome1, range(1, 201)))) print(list(filter(is_palindrome2, range(1, 201))))
輸出:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191] [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]
1.3 sorted
python內建的排序函式sorted,支援數字/字母/以及複雜物件排序,預設是從小到大排序,對於複雜物件的排序規則可以開發者自定義。參考下面的示例:
origin = [-1, 3, -5, 2, -4, 6] # 從小到大排序 a = sorted(origin) print(a) # 按abs絕對值,從小大到排序 a = sorted(origin, key=abs) print(a) # 從大到小排序 a = sorted(origin, reverse=True) print(a) origin = ["Xy", "Aa", "Bb", "dd", "cC", "aA", "Zo"] # 按字母ascii值從小到大排序 print(sorted(origin)) # 將字母轉大寫後的值排序(即:忽略大小寫) print(sorted(origin, key=str.upper)) # 將字母轉大寫後的值倒排序 print(sorted(origin, key=str.upper, reverse=True)) # 複雜物件排序 origin = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] def by_name(t): return t[0] # 按人名排序 print(sorted(origin, key=by_name)) def by_score(t): return t[1] # 按得分倒排 print(sorted(origin, key=by_score, reverse=True))
輸出:
[-5, -4, -1, 2, 3, 6] [-1, 2, 3, -4, -5, 6] [6, 3, 2, -1, -4, -5] ['Aa', 'Bb', 'Xy', 'Zo', 'aA', 'cC', 'dd'] ['Aa', 'aA', 'Bb', 'cC', 'dd', 'Xy', 'Zo'] ['Zo', 'Xy', 'dd', 'cC', 'Bb', 'Aa', 'aA'] [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)] [('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]
二、延遲計算/閉包
python的函式定義可以巢狀(即:函式內部再定義函式),利用這個特性很容易實現延遲計算:
import time def export1(month): print("export1 month:", month, " doing...") time.sleep(5) print("export1 done!") def export2(month): def do(): print("export2 month:", month, " doing...") time.sleep(5) print("export2 done!") return do export1(10) print("----------------") r2 = export2(10) print(r2) r2()
這裡我們模擬一個耗時的匯出功能(假設:要求傳入月份,然後匯出該月的報表資料),export1為常規版本,呼叫export1就會馬上執行。而export2則是返回一個內部函式do(),呼叫export2後,返回的是一個Function,並沒有實際執行(可以理解為: 返回的是業務處理演算法,而非處理結果),真正需要結果的時候,再來呼叫"返回函式"。
上面的程式碼輸出如下:
export1 month: 10 doing... export1 done! ---------------- <function export2.<locals>.do at 0x107a24a60> export2 month: 10 doing... export2 done!
閉包
很多語言都支援閉包特性,python中當然少不了這個,參考下面的示例:
def my_sqrt1(n): r = [] def do(): for i in range(1, n + 1): r.append(i ** 2) return r return do a = my_sqrt1(4) print(type(a)) b = a() print(type(b)) print(b)
輸出:
<class 'function'> <class 'list'> [1, 4, 9, 16]
閉包有一個經典的坑:不要在閉包函式中使用“值會發生變化的變數"(比如:for迴圈中的變數)。原因是:python中的閉包本質上是是"內部函式"延時計算,如果有迴圈變數,迴圈過程中閉包函式並不會執行,等迴圈結束了,閉包中引用的迴圈變數其實是迴圈結束後最終的值。說起來有點繞口,看下面的示例:
def my_sqrt2(n): r = [] for i in range(1, n + 1): def do(): r.append(i ** 2) return r return do a = my_sqrt2(4) print(type(a)) b = a() print(type(b)) print(b)
輸出:
<class 'function'> <class 'list'> [16]
解釋一下:呼叫a = my_sqrt2(4)時,my_sqrt2(4)馬上執行完了,這時候裡面的fox迴圈執行完了,最後i的值停在4,然後這個值被封閉在do函式裡,並沒有馬上執行。然後再呼叫a()時,這時候才真正呼叫do()函式,此時i值=4,所以最終r[]列表裡,就只追回了一個值4*4=16
如果非要使用迴圈變數,只能想招兒把這個迴圈變數,也封閉到一個內部函式裡,然後再使用,比如下面這樣:
def my_sqrt3(n): def f(j): def g(): return j ** 2 return g r = [] for i in range(1, n + 1): r.append(f(i)) return r a = my_sqrt3(4) print(type(a)) for x in a: print(x())
這個例子仔細研究下蠻有意思的,r.append(f(i)),列表裡追加的並非計算結果,而是f(j)裡返回的函式g,所以a = my_sqrt3(4)這裡,a得到的是一個function組成的list,然後list裡的每個g函式例項,都封閉了當次迴圈的變數i,因為閉包的緣故,i值已經被封印在g內部,不管外部的for迴圈如何變數,都不會影響函式g。
輸出如下:
<class 'list'> 1 4 9 16
關於閉包,最後再來看一個廖老師教程上的作業題,用閉包的寫法寫一個計數器:
def create_counter1(): r = [0] def counter(): r[0] += 1 return r[0] return counter count = create_counter1(); print([count(), count(), count()])
輸出:
[1, 2, 3]
對於有潔癖的程式設計師,可能會覺得要額外設定一個只儲存1個元素的list,有點浪費。可以換種寫法:
def create_counter2(): n = 0 def counter(): nonlocal n n += 1 return n return counter count = create_counter2(); print([count(), count(), count()])
輸出:
[1, 2, 3]
注意這裡有一個關鍵字nonlocal,號稱是python3新引入的關鍵字,為的是讓閉包的內部函式裡面,能讀寫內部函式外的變數。(但是在第1種寫法中,r=[0]不也是定義在外部麼?區別就是list是複雜的變數型別,而第2種寫法中n是簡單型別的變數,做為python初學者,不是很理解這個哲學思想^_~)
三、aop/裝飾器
aop是java生態體系中的精髓之一,而python裡同樣能做到,而且看上去更簡潔。
比如有一個加法函式:
def add1(i, j): return i + j
想在add1呼叫時,自動把入參,返回結果,以及執行時間都記錄下來,可以這麼做,再定義一個log函式(類似java中的aspect切面定義)
import time def log(fn): def do(*args, **kw): start = time.time() result = fn(*args, **kw) end = time.time() print("function=>", fn.__name__, ",args1=>", args, ",args2=>", kw, ",result=>", result, ",exec_time=>", (end - start) * 1000, "ms") return result return do
然後在add1函式上加上這個"註解"就可以了
@log def add1(i, j): return i + j print(add1(1, 2))
輸出:
function=> add1 ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 0.0030994415283203125 ms 3
如果aop本身的"切面"也需要傳引數進來,比如:在日誌前想附加一段特定的字首,可以參考下面這樣:
import time def log(fn): def do(*args, **kw): start = time.time() result = fn(*args, **kw) end = time.time() print("function=>", fn.__name__, ",args1=>", args, ",args2=>", kw, ",result=>", result, ",exec_time=>", (end - start) * 1000, "ms") return result return do def log2(log_prefix): def around(fn): def do(*args, **kw): start = time.time() result = fn(*args, **kw) end = time.time() print(log_prefix, ",function=>", fn.__name__, ",args1=>", args, ",args2=>", kw, ",result=>", result, ",exec_time=>", (end - start) * 1000, "ms") return result return do return around @log2("呼叫日誌:") @log def add2(i, j): time.sleep(1) return i + j print(add2(1, 2))
輸出:
function=> add2 ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 1000.8599758148193 ms 呼叫日誌: ,function=> do ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 1001.0490417480469 ms 3
注:這裡我們刻意把log,log2同時運用在add2上,從輸出上看,二個aspect都起作用了。
四、偏函式
還是拿add(i,j) 這個來說事兒吧,如果我們經常會遇到一個場景:想讓某個數字固定+10,在其它語言裡,通常是再定義一個類似add_10(i)的過載版本(java/c#都是這麼幹的),但是對於python來說,可以更優雅:
import functools def add(i, j): return i + j # 偏函式 add_10 = functools.partial(add, j=10) print(add(1, 2)) print(add_10(1))
輸出:
3 11
這種把引數列表中的某些常用項固定,然後再生成一個別名函式的玩法,就稱為偏函式
參考文件:
1、廖雪峰的python教程:函數語言程式設計