首頁>技術>

最近閱讀《流暢的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))

上下兩塊程式碼是等效效果;

12
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 「JetPack」這篇看完,Paging3大概可以入門了