python基础教程:函数装饰器详解
誰可以作為裝飾器(可以將誰編寫成裝飾器):
裝飾器可以去裝飾誰(誰可以被裝飾):
基礎:函數裝飾器的表現方式
假如你已經定義了一個函數funcA(),在準備定義函數funcB()的時候,如果寫成下面的格式:
@funcA def funcB():...表示用函數funcA()裝飾函數funcB()。當然,也可以認為是funcA包裝函數funcB。它等價于:
def funcB():...funcB = funcA(funcB)也就是說,將函數funcB作為函數funcA的參數,funcA會重新返回另一個可調用的對象(比如函數)并賦值給funcB。
所以,funcA要想作為函數裝飾器,需要接收函數作為參數,并且返回另一個可調用對象(如函數)。例如:
def funcA(F):......return Callable注意,函數裝飾器返回的可調用對象并不一定是原始的函數F,可以是任意其它可調用對象,比如另一個函數。但最終,這個返回的可調用對象都會被賦值給被裝飾的函數變量(上例中的funcB)。
函數可以同時被多個裝飾器裝飾,后面的裝飾器以前面的裝飾器處理結果為基礎進行處理:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' @decorator1 @decorator2 def func():...# 等價于 func = decorator1(decorator2(func))當調用被裝飾后的funcB時,將自動將funcB進行裝飾,并調用裝飾后的對象。所以,下面是等價的調用方式:
funcB() # 調用裝飾后的funcB funcA(funcB)()了解完函數裝飾器的表現后,大概也能猜到了,裝飾器函數可以用來擴展、增強另外一個函數。實際上,內置函數中staticmethod()、classmethod()和property()都是裝飾器函數,可以用來裝飾其它函數,在后面會學到它們的用法。
兩個簡單的例子
例如,函數f()返回一些字符串,現在要將它的返回結果轉換為大寫字母。可以定義一個函數裝飾器來增強函數f()。
def toupper(func):def wrapper(*args, **kwargs):result = func(*args, **kwargs)return result.upper()return wrapper@toupper def f(x: str): # 等價于f = toupper(f)return xres = f("abcd") print(res)上面toupper()裝飾f()后,調用f("abcd")的時候,等價于執行toupper(f)("abcd"),參數"abcd"傳遞給裝飾器中的wrapper()中的*args,在wrapper中又執行了f("abcd"),使得原本屬于f()的整個過程都完整了,最后返回result.upper(),這部分是對函數f()的擴展部分。
注意,上面的封裝函數wrapper()中使用了*args **kwargs,是為了確保任意參數的函數都能正確執行下去。
再比如要計算一個函數autodown()的執行時長,可以額外定義一個函數裝飾器timecount()。
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' import time# 函數裝飾器 def timecount(func):def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print(func.__name__, end - start)return resultreturn wrapper# 裝飾函數 @timecount def autodown(n: int):while n > 0:n -= 1# 調用被裝飾的函數 autodown(100000) autodown(1000000) autodown(10000000)執行結果:
autodown 0.004986763000488281 autodown 0.05684685707092285 autodown 0.5336081981658936上面wrapper()中的return是多余的,是因為這里裝飾的autodown()函數自身沒有返回值。但卻不應該省略這個return,因為timecount()可以去裝飾其它可能有返回值的函數。
@functools.wraps
前面的裝飾器代碼邏輯上沒有什么問題,但是卻存在隱藏的問題:函數的元數據信息丟了。比如doc、注解等。
比如下面的代碼:
@timecount def autodown(n: int):''' some docs '''while n > 0:n -= 1print(autodown.__name__) print(autodown.__doc__) print(autodown.__annotations__)執行結果為:
wrapper None {}所以,必須要將被裝飾函數的元數據保留下來??梢允褂胒unctools模塊中的wraps()裝飾一下裝飾器中的wrapper()函數。如下:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' import time from functools import wrapsdef timecount(func):@wraps(func)def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print(func.__name__, end - start)return resultreturn wrapper現在,再去查看autodown函數的元數據信息,將會得到被保留下來的內容:
autodownsome doc {'n': <class 'int'>}所以,wraps()的簡單用法是:向wraps()中傳遞的func參數,那么func的元數據就會被保留下來。
上面@wraps(func)裝飾wrapper的過程等價于:
def wrapper(*args, **kwargs):... wrapper = wraps(func)(wrapper)請注意這一點,因為在將類作為裝飾器的時候,經常會在__init__(self, func)里這樣使用:
class cls:def __init__(self, func):wraps(func)(self)...def __call__(self, *args, **kwargs):...解除裝飾
函數被裝飾后,如何再去訪問未被裝飾狀態下的這個函數?@wraps還有一個重要的特性,可以通過被裝飾對象的__wrapped__屬性來直接訪問被裝飾對象。例如:
autodown.__wrapped__(1000000)new_autodown = autodown.__wrapped__ new_autodown(1000000)上面的調用不會去調用裝飾后的函數,所以不會輸出執行時長。
注意,如果函數被多個裝飾器裝飾,那么通過__wrapped__,將只會解除第一個裝飾過程。例如:
@decorator1 @decorator2 @decorator3 def f():...當訪問f.__wrapped__()的時候,只有decorator1被解除,剩余的所有裝飾器仍然有效。注意,python 3.3之前是略過所有裝飾器。
下面是一個多裝飾的示例:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:531509025 尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' from functools import wrapsdef decorator1(func):@wraps(func)def wrapper(*args, **kwargs):print("in decorator1")return func(*args, **kwargs)return wrapperdef decorator2(func):@wraps(func)def wrapper(*args, **kwargs):print("in decorator2")return func(*args, **kwargs)return wrapperdef decorator3(func):@wraps(func)def wrapper(*args, **kwargs):print("in decorator3")return func(*args, **kwargs)return wrapper@decorator1 @decorator2 @decorator3 def addNum(x, y):return x+y返回結果:
in decorator1 in decorator2 in decorator3 5 in decorator2 in decorator3 5如果不使用functools的@wraps的__wrapped__,想要手動去引用原始函數,需要做的工作可能會非常多。所以,如有需要,直接使用__wrapped__去調用未被裝飾的函數比較好。
另外,并不是所有裝飾器中都使用了@wraps。
帶參數的函數裝飾器
函數裝飾器也是可以帶上參數的。
@decorator(x,y,z) def func():...它等價于:
func = decorator(x,y,z)(func)它并不是"天生"就這樣等價的,而是根據編碼規范編寫裝飾器的時候,通常會這樣。其實帶參數的函數裝飾器寫起來有點繞:先定義一個帶有參數的外層函數,它是外在的函數裝飾器,這個函數內包含了真正的裝飾器函數,而這個內部的函數裝飾器的內部又包含了被裝飾的函數封裝。也就是函數嵌套了一次又一次。
所以,結構大概是這樣的:
def out_decorator(some_args):...SOME CODE...def real_decorator(func):...SOME CODE...def wrapper(*args, **kwargs):...SOME CODE WITH func...return wrapperreturn real_decorator# 等價于func = out_decorator(some_args)(func) @out_decorator(some_args) def func():...下面是一個簡單的例子:
from functools import wrapsdef out_decorator(x, y, z):def decorator(func):@wraps(func)def wrapper(*args, **kwargs):print(x)print(y)print(z)return func(*args, **kwargs)return wrapperreturn decorator@out_decorator("xx", "yy", "zz") def addNum(x, y):return x+yprint(addNum(2, 3))參數隨意的裝飾器
根據前面介紹的兩種情況,裝飾器可以帶參數、不帶參數,所以有兩種裝飾的方式,要么是下面的(1),要么是下面的(2)。
@decorator # (1) @decorator(x,y,z) # (2)所以,根據不同的裝飾方式,需要編寫是否帶參數的不同裝飾器。
但是現在想要編寫一個將上面兩種參數方式統一起來的裝飾器。
可能第一想法是讓裝飾器參數默認化:
def out_decorator(arg1=X, arg2=Y...):def decorator(func):@wraps(func)def wrapper(*args, **kwargs):...return wrapperreturn decorator現在可以用下面兩種方式來裝飾:
@out_decorator() @out_decorator(arg1,arg2)雖然上面兩種裝飾方式會正確進行,但這并非合理做法,因為下面這種最通用的裝飾方式會錯誤:
@out_decorator為了解決這個問題,回顧下前面裝飾器是如何等價的:
# 等價于 func = decorator(func) @decorator def func():...# 等價于 func = out_decorator(x, y, z)(func) @out_decorator(x, y, z) def func():...上面第二種方式中,out_decorator(x,y,z)才是真正返回的內部裝飾器。所以,可以修改下裝飾器的編寫方式,將func也作為out_decorator()的其中一個參數:
from functools import wraps,partialdef decorator(func=None, arg1=X, arg2=Y):# 如果func為None,說明觸發的帶參裝飾器# 直接返回partial()封裝后的裝飾器函數if func is None:decorator_new = partial(decorator, arg1=arg1, arg2=arg2)return decorator_new#return partial(decorator, arg1=arg1, arg2=arg2)# 下面是裝飾器的完整裝飾內容@wraps(func)def wrapper(*args, **kwargs):...return wrapper上面使用了functools模塊中的partial()函數,它可以返回一個新的將某些參數"凍結"后的函數,使得新的函數無需指定這些已被"凍結"的參數,從而減少參數的數量。
現在,可以統一下面3種裝飾方式:
@decorator() @decorator(arg1=x,arg2=y) @decorator前兩種裝飾方式,等價的調用方式是decorator()(func)和decorator(arg1=x,arg2=y)(func),它們的func都為None,所以都會通過partial()返回通常的裝飾方式@decorator所等價的形式。
需要注意的是,因為上面的參數結構中包含了func=None作為第一個參數,所以帶參數裝飾時,必須使用keyword格式來傳遞參數,不能使用位置參數。
下面是一個簡單的示例:
from functools import wraps, partialdef decorator(func=None, x=1, y=2, z=3):if func is None:return partial(decorator, x=x, y=y, z=z)@wraps(func)def wrapper(*args, **kwargs):print("x: ", x)print("y: ", y)print("z: ", z)return func(*args, **kwargs)return wrapper下面3種裝飾方式都可以:
@decorator def addNum(a, b):return a + b print(addNum(2, 3))print("=" * 40)@decorator() def addNum(a, b):return a + b print(addNum(2, 3))print("=" * 40)# 必須使用關鍵字參數進行裝飾 @decorator(x="xx", y="yy", z="zz") def addNum(a, b):return a + b print(addNum(2, 3))返回結果:
x: 1 y: 2 z: 3 5 ==================== x: 1 y: 2 z: 3 5 ==================== x: xx y: yy z: zz 5總結
以上是生活随笔為你收集整理的python基础教程:函数装饰器详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python基础教程:字典(当索引不好用
- 下一篇: Python基础教程:自定义迭代器