快速理解並使用Python Decorator(裝飾器)

前言

裝飾器的寫法總共有四種:

  1. 不含參數的function decorator
  2. 含參數的function decorator
  3. 不含參數的class decorator
  4. 含參數的class decorator

基本上用起來效果都一樣,沒有分好壞
直接寫function當然簡單又快速

而以可讀性來看,個人較偏好class的寫法,畢竟可讀性也是很重要的一環
在Stack Overflow查到的回答亦如此(詳細可至文末參考資料)

若對參數傳遞(call by reference)不熟悉的話,function這塊可能會看得有點吃力。可從class理解起並使用,熟悉後再回頭看function的寫法,應該就會恍然大悟了!

先附上範例寫法,直接拷貝就可以使用
原理說明放在後面

文章有點長,可以點擊左方的文章目錄迅速定位至需要的片段

使用時應注意事項

  1. 使用decorator時,在呼叫.__name__時,會變成decorator的名字,在debug時會難以查找問題。須from functools import wraps,來重新指向,此為Python原生功能,且本身也是由decorator來實現
  2. 使用Class-based Decorator的話,就不必functools.wraps來重新指向.__name__了!
  3. 累加多個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__裡,而傳入的function f寫在__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

  1. 再多包一層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 reference

    call 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)

  1. 先將myFunc傳入,此時尚未執行裡面的wrapper
  2. return wrapper地址,因遇到@,執行wrapper()
  3. 因使用@特殊執行,wrapper會接收myFunc本身的參數(*args,**kwargs),接著開始執行收到的地址(即wrapper)
  4. 在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