最近閱讀《流暢的python》看見其用函式寫裝飾器部分寫的很好,想寫一些自己的讀書筆記。眾所周知,裝飾器是python學習過程中的一道門檻,初學者學習時往往是知其然,不知其所以然,這樣的結果是導致一段時間後會遺忘掉該部分內容,只好再次去學習,拉高了學習成本。
想學好python的裝飾器,需要明白一下幾點;
1:閉包1)函式巢狀
2)內部函式使用外部函式的變數
3)外部函式的返回值為內部函式
接下來看看《流暢的python》中的例子,我稍微修改了一下:
>>> def make_averager(series=[]):... def averager(new_value):... series.append(new_value)... total = sum(series)... return total/len(series)... return averager...>>> avg = make_averager()>>> avg<function make_averager.<locals>.averager at 0x10b82cb00>>>> avg(10)10.0>>> avg(11)10.5>>> avg(12)11.0
函式 make_averager 實現了一個 計算當前所有數字的平均值的功能,不斷的新增一個值,然後計算當前的平均值。
avg這個物件記憶體地址指向了make_averager這個函式的內部函式中,而且avg透過不斷的新增值進行平均值計算,按理說在這個內部函式沒有儲存new_value的空間,而且在make_averager對avg賦值後,函式返回後series這個變數也應該消失了,但是avg卻依然可以進行計算。
這就是閉包,內部函式averager使用外面的自由變數,也就是屬於make_averager的區域性變數series
>>> avg.__code__.co_varnames('new_value', 'total')>>> avg.__code__.co_freevars('series',)
可以發現avg的自由變數是make_averager的區域性變數,就是說閉包裡的內部函式可以使用外部函式的變數,即我們上面提到的第二點:“內部函式使用外部函式的變數”, 注:自由變數只能read,並不能write,不然會提示本地變數並沒有賦值的錯誤,我們舉的例子沒遇到這個問題,因為我們沒有給 series 賦值,我們只是調 用 series.append,並把它傳給 sum 和 len。也就是說,我們利用了 列表是可變的物件這一事實 。下圖是書中提供的閉包範圍圖:
2:裝飾器的實現所謂裝飾器,就是在不改變基礎函式的功能上再次給它封裝一層,達到我們想要的目的,接下來我舉個簡單的例子:
deco_demo.py
1 def col(func): 2 def inner(*args, **kwargs): 3 print(func.__name__) 4 print(locals()) 5 print(inner.__code__.co_varnames) 6 print(inner.__code__.co_freevars) 7 return func(*args, **kwargs) 8 return inner 9 10 11 @col 12 def new_add(x): 13 return x+2 14 15 16 def new_add_1(x): 17 return x+3 18 19 20 print(new_add(3)) 21 22 new_add_1 = col(new_add_1) 23 print(new_add_1(3))
下方是它的返回結果:
new_add{'args': (3,), 'kwargs': {}, 'func': <function new_add at 0x10d32aa70>, 'inner': <function col.<locals>.inner at 0x10d32acb0>}('args', 'kwargs')('func', 'inner')5new_add_1{'args': (3,), 'kwargs': {}, 'func': <function new_add_1 at 0x10d32add0>, 'inner': <function col.<locals>.inner at 0x10d32a8c0>}('args', 'kwargs')('func', 'inner')6
1-8:是定義的一個簡單裝飾器,
3:列印當被裝飾函式的名字
4:列印inner這個內部函式中的所有變數
5:列印當前inner的區域性變數;
6:則列印自由變數;
11-13:修飾了一個簡單函式
16,22,23:@這個語法糖,背後實現的過程;
也就是說 col(new_add) 返回的是當前的內部函式的記憶體地址,而這個呼叫這個內部函式時會使用自由變數func即col的區域性變數,進而達到裝飾器的目的;
有引數的裝飾器實現 既然無引數的裝飾器即@col ,透過內部函式的方式裝飾基礎函式,那麼我們呼叫有引數的裝飾器 則可以在原本的基礎即函式col再封裝一層函式,使其達到可以透過裝飾器傳引數的目的
1 from functools import wraps 2 3 4 def col(string="hello world"): 5 def decorate(func): 6 @wraps(func) 7 def inner(*args, **kwargs): 8 print(string) 9 return func(*args, **kwargs) 10 return inner 11 return decorate 12 13 14 @col() 15 def new_add(x): 16 return x+2 17 18 19 @col("hello python") 20 def new_add_1(x): 21 return x+3 22 23 24 def new_add_2(x): 25 return x+4 26 27 28 print(new_add(1)) 29 print(new_add_1(1)) 30 31 32 new_add_2 = col("hello china")(new_add_2) 33 print(new_add_2(1))
匯入wrap是為了修復這個裝飾器的名稱, new_add.__name__ 呼叫時指向被裝飾的函式,而不是內部函式,有興趣的小夥伴可以去了解一下;
4-11:實現了一個帶引數的裝飾器,最外層返回的是我們真正的裝飾器;
32-33:則是@這個裝飾器語法糖背後的實現過程
可以發現new_add與new_add_1這兩個函式的裝飾器是兩個不同值,而我們的裝飾器也返回了不同的對應情況
hello world3hello python4hello china5
簡而言之:裝飾器就是在我們需要新增功能的函式上進而封裝一層,而python的語法糖@背後,幫助我們省略掉了這些賦值的過程;
1 registry = [] 2 3 4 def register(func): 5 print(f"running register {func}") 6 registry.append(func) 7 return func 8 9 10 @register 11 def f1(): 12 print('running f1()') 13 14 15 @register 16 def f2(): 17 print('running f2()') 18 19 20 def f3(): 21 print('running f3()') 22 23 24 def main(): 25 print('running main()') 26 print('regisry ->', registry) 27 f1() 28 f2() 29 f3() 30 31 32 if __name__ == '__main__': 33 main()
當作獨立指令碼執行時:
running register <function f1 at 0x103f9dcb0>running register <function f2 at 0x103f9ddd0>running main()regisry -> [<function f1 at 0x103f9dcb0>, <function f2 at 0x103f9ddd0>]running f1()running f2()running f3()
被當作模組匯入時:
>>> import registrationrunning register <function f1 at 0x1005a2710>running register <function f2 at 0x1005a2b90>>>> registration.registry[<function f1 at 0x1005a2710>, <function f2 at 0x1005a2b90>]
該段程式碼的裝飾器主要功能是:記錄了被裝飾函式的個數,通常是web框架以這種方式把函式註冊到中央註冊器的某處。
總結:可以發現裝飾器無論是作為模組被匯入,還是單獨的指令碼執行,它都是優先執行的;
4:裝飾器的常用模組之前介紹的function.wraps不用說了,接下來介紹兩種神奇的裝飾器;
1:singledispatch何為singledispatch ?
就是在不改變函式本身的功能上覆用該函式,達到重複使用函式名的目的,有點類似多型的感覺;可以把整體方案拆分成多個模組,甚至可以為你無法修改的類提供專門的函式。使用@singledispatch 裝飾的普通函式會變成泛函數(generic function);根據第一個引數的型別,以不同方式執行相同操作的一組函式
1 from functools import singledispatch 2 3 4 @singledispatch 5 def hello(obj): 6 print(obj) 7 8 9 @hello.register(str) 10 def _(text): 11 print("hello world "+text) 12 13 14 @hello.register(int) 15 def _(n): 16 print(n) 17 18 19 hello({"what": "say"}) 20 print('*'*30) 21 hello('dengxuan') 22 print('*'*30) 23 hello(123)
{'what': 'say'}******************************hello world dengxuan******************************123
從該段程式碼中我們可以發現,當使用singledispatch這個裝飾器時,函式hello可以根據不同的引數返回不同的結果。這樣的好處就是極大的減少程式碼中的if/elif/else,並且可以複用函式名稱 _ (下橫線代表沒用),降低了程式碼的耦合度,達到了多型的效果。
2:lru_cache根據書上原話:
functools.lru_cache 是非常實用的裝飾器,它實現了備忘 (memoization)功能。這是一項最佳化技術,它把耗時的函式的結果儲存 起來,避免傳入相同的引數時重複計算。LRU 三個字母是“Least Recently Used”的縮寫,表明快取不會無限制增長,一段時間不用的快取 條目會被扔掉。
1 from my_tools.runtime import clock 2 import functools 3 4 5 @functools.lru_cache() 6 @clock 7 def fibonacci(n): 8 if n < 2: 9 return n 10 return fibonacci(n-2)+fibonacci(n-1) 11 12 13 if __name__ == '__main__': 14 print(fibonacci(6))
第5行:註釋funtools.lru_cache()
返回結果:
[0.00000046] fibonacci(0) -> 0[0.00000053] fibonacci(1) -> 1[0.00006782] fibonacci(2) -> 1[0.00000030] fibonacci(1) -> 1[0.00000035] fibonacci(0) -> 0[0.00000037] fibonacci(1) -> 1[0.00001312] fibonacci(2) -> 1[0.00002514] fibonacci(3) -> 2[0.00010535] fibonacci(4) -> 3[0.00000030] fibonacci(1) -> 1[0.00000030] fibonacci(0) -> 0[0.00000037] fibonacci(1) -> 1[0.00001209] fibonacci(2) -> 1[0.00002376] fibonacci(3) -> 2[0.00000028] fibonacci(0) -> 0[0.00000038] fibonacci(1) -> 1[0.00001210] fibonacci(2) -> 1[0.00000028] fibonacci(1) -> 1[0.00000036] fibonacci(0) -> 0[0.00000034] fibonacci(1) -> 1[0.00001281] fibonacci(2) -> 1[0.00002466] fibonacci(3) -> 2[0.00004897] fibonacci(4) -> 3[0.00008414] fibonacci(5) -> 5[0.00020196] fibonacci(6) -> 88
當取消掉第5行註釋時;
[0.00000040] fibonacci(0) -> 0[0.00000049] fibonacci(1) -> 1[0.00008032] fibonacci(2) -> 1[0.00000066] fibonacci(3) -> 2[0.00009398] fibonacci(4) -> 3[0.00000063] fibonacci(5) -> 5[0.00010943] fibonacci(6) -> 88
可以發現,lru_cache()這個裝飾器,極大的提高了計算效能;
maxsize 引數指定儲存多少個呼叫的結果。快取滿了之後,舊的結果會被扔掉,騰出空間。為了得到最佳效能,maxsize 應該設為 2 的 冪。typed 引數如果設為 True,把不同引數型別得到的結果分開儲存,即把通常認為相等的浮點數和整數引數(如 1 和 1.0)區分開。順 便說一下,因為 lru_cache 使用字典儲存結果,而且鍵根據呼叫時傳 入的定位引數和關鍵字引數建立,所以被 lru_cache 裝飾的函式,它的所有引數都必須是可雜湊的。
5:多重灌飾器@d1@d2def f(): print('hello world') ###########################def f(): print("hello world")f = d1(d2(f))
上下兩塊程式碼是等效效果;