快速理解並使用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的裝飾器先執行
    1
    2
    3
    4
    5
    @second_exec
    @first_exec
    def test():
    pass
    #會先執行first_exec,再執行second_exec

Class-based Decorator

  • 不必import functools.wraps來重新指向.__name__
  • 可讀性較佳

    不含參數的Class-based Decorator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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()

輸出結果

1
2
3
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)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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()

輸出結果

1
2
3
# 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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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()

輸出結果

1
2
3
# do something before calling function myFunc
# 主程式
# do something after calling function myFunc

含參數的Function-based Decorator

  1. 再多包一層function來傳遞參數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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()

輸出結果

1
2
3
#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記錄這檔事

用程式來表達就是:

1
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來說明程式執行順序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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