快速理解並使用Python Decorator(裝飾器)
前言
裝飾器的寫法總共有四種:
- 不含參數的function decorator
- 含參數的function decorator
- 不含參數的class decorator
- 含參數的class decorator
基本上用起來效果都一樣,沒有分好壞
直接寫function當然簡單又快速
而以可讀性來看,個人較偏好class的寫法,畢竟可讀性也是很重要的一環
在Stack Overflow查到的回答亦如此(詳細可至文末參考資料)
若對參數傳遞(call by reference)不熟悉的話,function這塊可能會看得有點吃力。可從class理解起並使用,熟悉後再回頭看function的寫法,應該就會恍然大悟了!
先附上範例寫法,直接拷貝就可以使用
原理說明放在後面
文章有點長,可以點擊左方的文章目錄迅速定位至需要的片段
使用時應注意事項
- 使用decorator時,在呼叫
.__name__
時,會變成decorator的名字,在debug時會難以查找問題。須from functools import wraps
,來重新指向,此為Python原生功能,且本身也是由decorator來實現 - 使用Class-based Decorator的話,就不必functools.wraps來重新指向
.__name__
了! - 累加多個decorator時,執行順序由下至上,即從靠近function的裝飾器先執行
@second_exec @first_exec def test(): pass #會先執行first_exec,再執行second_exec
Class-based Decorator
- 不必import
functools.wraps
來重新指向.__name__
了 - 可讀性較佳
不含參數的Class-based Decorator
輸出結果class decorateClass(object): def __init__(self, f): self.f = f def __call__(self, *args, **kwargs): print(f"do something before calling function {self.f.__name__}") self.f(*args, **kwargs) print(f"do something after calling function {self.f.__name__}") @decorateClass def myFunc(): print('主程式') if __name__ == '__main__': myFunc()
do something before calling function myFunc 主程式 do something after calling function myFunc
含參數的Class-based Decorator
- 比較不同的是,裝飾器傳入的參數放在
__init__
裡,而傳入的functionf
寫在__call__
裡面 __call__
裡面再多包一層function來傳遞被裝飾function(即myFunc)
本身的參數(即*args,**kwargs)
輸出結果class decorateClass(object): def __init__(self, para1,para2): self.para1 = para1 self.para2 = para2 def __call__(self, f): def wrapper(*args,**kwargs): print(f"do something before calling function {f.__name__} with {self.para1}") f(*args, **kwargs) print(f"do something after calling function {f.__name__} with {self.para2}") return wrapper @decorateClass('傳入參數1','傳入參數2') def myFunc(): print('主程式') if __name__ == '__main__': myFunc()
# do something before calling function myFunc with 傳入參數1 # 主程式 # do something after calling function myFunc with 傳入參數2
Function-based Decorator
- 要
from functools import wraps
,並@wraps(f)
來重新指向.__name__
,否則trace時會難以debug - 若不熟悉參數傳遞的形式 (call by value、call by reference)的話,可能會看不懂運作原理。建議先行了解後再研究這塊程式碼
不含參數的Function-based Decorator
輸出結果from functools import wraps def decorate(f): @wraps(f) def wrapper(*args,**kwargs): print(f'do something before calling function {f.__name__}') f(*args,**kwargs) print(f'do something after calling function {f.__name__}') return wrapper @decorate def myFunc(): print('主程式') if __name__ == '__main__': myFunc()
# do something before calling function myFunc # 主程式 # do something after calling function myFunc
含參數的Function-based Decorator
- 再多包一層function來傳遞參數
輸出結果from functools import wraps def decorate(para1,para2): def outer_wrapper(f): @wraps(f) def wrapper(*args,**kwargs): print(f'do something before calling function {f.__name__} with {para1}') f(*args,**kwargs) print(f'do something after calling function {f.__name__} with {para2}') return wrapper return outer_wrapper @decorate('傳入參數1','傳入參數2') def myFunc(): print('主程式') if __name__ == '__main__': myFunc()
#do something before call function myFunc with 傳入參數1 #主程式 #do something after call function myFunc with 傳入參數2
原理說明
先簡單科普一下call by value、call by referencecall by value
把值複製一份傳進去,在function裡修改該值,不影響外面傳入的值call by reference
就是把儲存該值的地址傳進去,在function裡修改該值,會影響外面傳入的值
具體細節就煩請Google了,不然文章就有點太冗長了QQ
裝飾器原理說明
其實裝飾器,就是把你的function再包一層function
在執行你的function之前及之後,再多做些其他動作,比如記錄log、關開signals
避免一直在各functions內部一直重複寫開關、log記錄這檔事
用程式來表達就是:
wrapper = decorate(myFunc)
底層實際上的實現方式可能不是這樣
看了許多篇文章還是不太懂到底怎麼傳遞
傳遞方式不懂的話,根本就寫不出來啊啊啊~~~
最後是用這樣的方式才算是比較理解,也總算可以寫出自己要用的裝飾器了~
如果有理解錯誤的地方煩請指正,謝謝
我們在執行function時,會在後方加()
,表示要執行
而裝飾器的寫法@
,可以想成另一種特殊執行function的方式
用@
來執行的function,必須接收另一function的地址,且會將被裝飾的參數丟進去(即myFunc的參數*args,**kwargs
)
- 先將myFunc傳入,此時尚未執行裡面的wrapper
- return wrapper地址,因遇到
@
,執行wrapper() - 因使用
@
特殊執行,wrapper會接收myFunc本身的參數(*args,**kwargs
),接著開始執行收到的地址(即wrapper) - 在wrapper中執行到
f
(即myFunc)時,要記得把myFunc本身的參數還給他
以下以function-based decorator來說明程式執行順序
from functools import wraps
def decorate(f):
@wraps(f)
def wrapper(*args,**kwargs): #4. 進來執行warpper
print(f'do something before calling function {f.__name__}') #5. myFunc被執行前要做的事
f(*args,**kwargs) #6. 此處才執行真正被呼叫的myFunc(),再將原本丟給myFunc的參數還給他
print(f'do something after calling function {f.__name__}') #7. myFunc被執行後要做的事
return wrapper #3. @呼叫僅接收function地址,會自動將被裝飾函式的參數傳入(即myFunc的參數var1)
@decorate #2. 執行前發現有裝飾器,先呼叫裝飾器decorate
def myFunc(var1):
print(f'主程式{var1}')
if __name__ == '__main__':
myFunc('傳進去') #1. 執行myFunc
後記
雖然這種理解方式似乎有點歪
不過重點在於知道該如何撰寫裝飾器來簡化你的程式碼
最大的癥結點大概就是卡在參數不知道怎麼傳
就算不理解底層原理也沒關係,會複製貼上改成自己要用的其實就很足夠了!
除非是想要開發類似tenacity的爬蟲輔助套件,完全使用裝飾器來作業,那就真的需要更深入了解了!
參考資料
Stack Overflow/Python decorator best practice, using a class vs a function
[翻译]理解PYTHON中的装饰器
Python Decorator 四種寫法範例 Code