基礎:函數裝飾器的表現方式 假如你已經定義了一個函數funcA(),在準備定義函數funcB()的時候,如果寫成下麵的格式: 表示用函數funcA()裝飾函數funcB()。當然,也可以認為是funcA包裝函數funcB。它等價於: 也就是說,將函數funcB作為函數funcA的參數,funcA會重 ...
先混個眼熟
誰可以作為裝飾器(可以將誰編寫成裝飾器):
- 函數
- 方法
- 實現了
__call__
的可調用類
裝飾器可以去裝飾誰(誰可以被裝飾):
- 函數
- 方法
- 類
基礎:函數裝飾器的表現方式
假如你已經定義了一個函數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)。
函數可以同時被多個裝飾器裝飾,後面的裝飾器以前面的裝飾器處理結果為基礎進行處理:
@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 x
res = 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()。
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 result
return 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 -= 1
print(autodown.__name__)
print(autodown.__doc__)
print(autodown.__annotations__)
執行結果為:
wrapper
None
{}
所以,必須要將被裝飾函數的元數據保留下來。可以使用functools模塊中的wraps()裝飾一下裝飾器中的wrapper()函數。如下:
import time
from functools import wraps
def timecount(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end - start)
return result
return wrapper
現在,再去查看autodown函數的元數據信息,將會得到被保留下來的內容:
autodown
some 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之前是略過所有裝飾器。
下麵是一個多裝飾的示例:
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("in decorator1")
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("in decorator2")
return func(*args, **kwargs)
return wrapper
def 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 wrapper
return real_decorator
# 等價於func = out_decorator(some_args)(func)
@out_decorator(some_args)
def func():...
下麵是一個簡單的例子:
from functools import wraps
def 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 wrapper
return decorator
@out_decorator("xx", "yy", "zz")
def addNum(x, y):
return x+y
print(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 wrapper
return 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,partial
def 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()函數,它可以返回一個新的將某些參數"凍結"後的函數,使得新的函數無需指定這些已被"凍結"的參數,從而減少參數的數量。如果不知道這個函數,參考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, partial
def 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