快速理解並使用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