裝飾器的語法為 @dec_name ,置於函數定義之前。如: import atexit @atexit.register def goodbye(): print('Goodbye!') print('Script end here') atexit.register 是一個裝飾器,它的作用是將被 ...
裝飾器的語法為 @dec_name
,置於函數定義之前。如:
import atexit @atexit.register def goodbye(): print('Goodbye!') print('Script end here')
atexit.register
是一個裝飾器,它的作用是將被裝飾的函數註冊為在程式結束時執行。函數 goodbye
是被裝飾的函數。
程式的運行結果是:
Script end here Goodbye!
可見函數 goodbye
在程式結束後被自動調用。
另一個常見的例子是 @property
,裝飾類的成員函數,將其轉換為一個描述符。
class Foo: @property def attr(self): print('attr called') return 'attr value' foo = Foo()
等價語法
語句塊
@atexit.register def goodbye(): print('Goodbye!')
等價於
def goodbye(): print('Goodbye!') goodbye = atexit.register(goodbye)
這兩種寫法在作用上完全等價。
從第二種寫法,更容易看出裝飾器的原理。裝飾器實際上是一個函數(或callable),其輸入、返回值為:
說明 | 示例中的對應 | |
---|---|---|
輸入 | 被裝飾的函數 | goodbye |
返回值 | 變換後的函數或任意對象 |
返回值會被賦值給原來指向輸入函數的變數,如示例中的 goodbye
。此時變數 goodbye
將指向裝飾器的返回值,而不是原來的函數定義。返回值一般為一個函數,這個函數是在輸入參數函數添加了一些額外操作的版本。
如下麵這個裝飾器對原始函數添加了一個操作:每次調用這個函數時,列印函數的輸入參數及返回值。
def trace(func): def wrapper(*args, **kwargs): 1 print('Enter. Args: %s, kwargs: %s' % (args, kwargs)) 2 rv = func(*args, **kwargs) 3 print('Exit. Return value: %s' % (rv)) 4 return rv return wrapper @trace def area(height, width): print('area called') return height * width area(2, 3) 5
- 1 :定義一個新函數,這個函數將作為裝飾器的返回值,來替換原函數
- 2, 4 : 列印輸入參數、返回值。這是這個裝飾器所定義的操作
- 3 :調用原函數
- 5 :此時
area
實際上是 1 處定義的wrapper
函數
程式的運行結果為:
Enter. Args: (2, 3), kwargs: {} area called Exit. Return value: 6
如果不使用裝飾器,則必須將以上列印輸入參數及返回值的語句直接寫在 area
函數里,如:
def area(height, width): print('Enter. Args: %s, %s' % (height, width)) print('area called') rv = height * width print('Exit. Return value: %s' % (rv)) return rv area(2, 3)
程式的運行結果與使用裝飾器時相同。但使用裝飾器的好處為:
- 列印輸入參數及返回值這個操作可被重用
如對於一個新的函數
foo
,裝飾器trace
可以直接拿來使用,而無須在函數內部重覆寫兩條print
語句。@trace def foo(val): return 'return value'
一個裝飾器實際上定義了一種可重覆使用的操作
- 函數的功能更單純
area
函數的功能是計算面積,而調試語句與其功能無關。使用裝飾器可以將與函數功能無關的語句提取出來。 因此函數可以寫地更小。使用裝飾器,相當於將兩個小函數組合起來,組成功能更強大的函數
修正名稱
以上例子中有一個缺陷,函數 area
被 trace
裝飾後,其名稱變為 wrapper
,而非 area
。 print(area)
的結果為:
<function wrapper at 0x10df45668>
wrapper
這個名稱來源於 trace
中定義的 wrapper
函數。
可以通過 functools.wraps
來修正這個問題。
from functools import wraps def trace(func): @wraps(func) def wrapper(*args, **kwargs): print('Enter. Args: %s, kwargs: %s' % (args, kwargs)) rv = func(*args, **kwargs) print('Exit. Return value: %s' % (rv)) return rv return wrapper @trace def area(height, width): print('area called') return height * width
即使用 functools.wraps
來裝飾 wrapper
。此時 print(area)
的結果為:
<function area at 0x10e8371b8>
函數的名稱能夠正確顯示。
接收參數
以上例子中 trace
這個裝飾器在使用時不接受參數。如果想傳入參數,如傳入被裝飾函數的名稱,可以這麼做:
from functools import wraps def trace(name): def wrapper(func): @wraps(func) def wrapped(*args, **kwargs): print('Enter %s. Args: %s, kwargs: %s' % (name, args, kwargs)) rv = func(*args, **kwargs) print('Exit %s. Return value: %s' % (name, rv)) return rv return wrapped return wrapper @trace('area') def area(height, width): print('area called') return height * width area(2, 3)
程式的運行結果為:
Enter area. Args: (2, 3), kwargs: {} area called Exit area. Return value: 6
將函數名稱傳入後,在日誌同時列印出函數名,日誌更加清晰。
@trace('area')
是如何工作的?
這裡其實包含了兩個步驟。 @trace('area')
等價於:
dec = trace('area') @dec def area(height, width): ...
即先觸發函數調用 trace('area')
,得到一個返回值,這個返回值為 wrapper
函數。 而這個函數才是真正的裝飾器,然後使用這個裝飾器裝飾函數。
多重裝飾器
裝飾器可以疊加使用,如:
@dec1 @dec2 def foo():pass
等價的代碼為:
def foo():pass foo = dec2(foo) foo = dec1(foo)
即裝飾器依次裝飾函數,靠近函數定義的裝飾器優先。相當於串聯起來。
如果你一路讀到這裡,我相信你已經掌握了關於Python 裝飾器80%的知識,並能夠應用到工作學習中。你知道了裝飾器的工作原理,以及如何自己編寫一個裝飾器,及避免常見的編寫錯誤。
如果你仍然有疑問,歡迎留言討論!(如果你覺得這篇文章有用,請點下方推薦按鈕:p)