本文的文字及圖片來源於網絡,僅供學習、交流使用,不具備任何商業用途,若有問題請及時聯繫咱們以做處理html
最近閱讀《流暢的python》看見其用函數寫裝飾器部分寫的很好,想寫一些本身的讀書筆記。衆所周知,裝飾器是python學習過程當中的一道門檻,初學者學習時每每是知其然,不知其因此然,這樣的結果是致使一段時間後會遺忘掉該部份內容,只好再次去學習,拉高了學習成本。python
想學好python的裝飾器,須要明白一下幾點;web
1:閉包緩存
1)函數嵌套網絡
2)內部函數使用外部函數的變量閉包
3)外部函數的返回值爲內部函數app
接下來看看《流暢的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') 5 new_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 world 3 hello python 4 hello china 5
間而言之:裝飾器就是在咱們須要添加功能的函數上進而封裝一層,而python的語法糖@背後,幫助咱們省略掉了這些賦值的過程;
3:裝飾器什麼時候調用
關於裝飾器什麼時候運行,咱們分兩種狀況討論,一種是看成腳本運行時,另外一種是看成模塊被導入時;
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 registration running 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) -> 8 8
當取消掉第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) -> 8 8
能夠發現,lru_cache()這個裝飾器,極大的提升了計算性能;
maxsize 參數指定存儲多少個調用的結果。緩存滿了以後,舊的結果會被扔掉,騰出空間。爲了獲得最佳性能,maxsize 應該設爲 2 的 冪。typed 參數若是設爲 True,把不一樣參數類型獲得的結果分開保存,即把一般認爲相等的浮點數和整數參數(如 1 和 1.0)區分開。順 便說一下,由於 lru_cache 使用字典存儲結果,並且鍵根據調用時傳 入的定位參數和關鍵字參數建立,因此被 lru_cache 裝飾的函數,它的全部參數都必須是可散列的。
5:多重裝飾器
@d1 @d2 def f(): print('hello world') ########################### def f(): print("hello world") f = d1(d2(f))
上下兩塊代碼是等效效果;
想要獲取更多Python學習資料能夠加QQ:2955637827私聊或加Q羣630390733你們一塊兒來學習討論吧!