函數裝飾器裝飾方法
函數裝飾器裝飾普通函數已經很容易理解了:
@decorator
def func():...
#等價於
def func():...
func = decorator(func)
如果裝飾器是帶參裝飾器,那么等價的形式大概是這樣的(和裝飾器的編碼有關,但最普遍的編碼形式如下):
@decorator(x, y, z)
def func():...
# 等價於
def func():...
func = decorator(x, y, z)(func)
這樣的函數裝飾器也可以去裝飾類中的方法。看下面的方法裝飾形式:
class cls:
@decorator
def method(self,arg1,arg2):
...
它等價於:
class cls:
def method(self,arg1,arg2):
...
method = decorator(method)
在decorator的編碼中,仍然像普通的函數裝飾器一樣編寫即可。例如:
def decorator(F):
@wraps(F)
def wrapper(*args, **kwargs):
... # args[0] = self_instance
# args[1]開始才是手動傳給method的參數
return wrapper
但必須要考慮到method的第一個參數self,所以包裝器wrapper()的第一個參數也是self。
如此一來,函數裝飾器既可以裝飾函數,又可以裝飾方法。
下面是一個示例:
from functools import wraps
def decorator(F):
@wraps(F)
def wrapper(*args, **kwargs):
result = F(*args, **kwargs)
print(args)
return result
return wrapper
@decorator
def func(x,y):
return x + y
print(func(3, 4))
print("-" * 30)
class cls:
@decorator
def method(self, x, y):
return x + y
c = cls()
print(c.method(3, 4))
輸出結果:
(3, 4)
7
------------------------------
(<__main__.cls object at 0x01DF1C50>, 3, 4)
7
讓類稱為裝飾器
不僅函數可以作為裝飾器,類也可以作為裝飾器去裝飾其它對象。
如何讓類作為裝飾器
要讓類作為裝飾器,先看裝飾的形式:
class Decorator:
...
@Decorator
def func():
...
func(arg1, arg2)
如果成功裝飾,那么它等價於:
def func(): ...
func = Decorator(func)
func(arg1, arg2)
這和函數裝飾器看上去是一樣的,但區別在於Decorator這里是一個類,而不是函數,且Decorator(func)
表示的是創建一個Decorator類的實例對象,所以這里賦值符號左邊的func是一個對象。所有后面的func(arg1, arg2)
是調用對象,而不是調用函數。
要讓實例對象成為可調用對象,它必須實現__call__
方法,所以應該在Decorator類中定義一個__call__
。而且每次調用實例對象的時候,都是在調用__call__
,這里的__call__
對等於函數裝飾器中的包裝器wrapper
,所以它的參數和邏輯應當和wrapper一樣。
如下:
class Decorator():
def __call__(self, *args, **kwargs):
...
再看func = Decorator(func)
,func是Decorator類創建實例的參數,所以Decorator類還必須實現一個__init__
方法,接受func作為參數:
class Decorator:
def __init__(self, func):
...
def __call__(self, *args, **kwargs):
...
元數據問題
這樣的裝飾器已經能正常工作了,但是會丟失func的元數據信息。所以,必須使用functools的wraps()保留func的元數據:
from functools import wraps
class Decorator:
def __init__(self, func):
wraps(func)(self)
...
def __call__(self, *args, **kwargs):
...
為什么是wraps(func)(self)
?這里顯然不能@wraps(func)
的方式裝飾包裝器,所以只能使用wraps()的原始函數形式。在wraps()裝飾函數包裝器wrapper的時候,@wraps(func)
等價於wrapper = wraps(func)(wrapper)
,所以這里wraps(func)(self)
的作用也是很明顯的:保留func的元數據,並裝飾self。被裝飾的self是什么?是Decorator的實例對象,因為Decorator類實現了__call__
,所以self是可調用的,所以這里的self類似於函數裝飾器返回的wrapper函數(實際上self是Decorator(func)返回的各個實例對象)。
類作為裝飾器的參數問題
雖然self是Decorator的可調用實例對象,但是上面的代碼中self並不具有func屬性,也就是說無法從self去調用func()函數,這似乎使得整個過程都崩塌了:廢了老大的勁去解決各種裝飾器上的問題,結果卻不能調用被裝飾的函數。
有兩種方式可以解決這個問題:
- 在
__init__
中使用self.func = func
保留func對象作為裝飾器的一個屬性 - 在使用wraps()后直接在包裝器
__call__
中使用__wrapped__
調用原始func函數
這兩種方式其實是等價的,因為self.func
和__wrapped__
都指向原始的函數。
def __init__(self,func):
wraps(func)(self)
self.func = func
def __call__(self, *args, **kwargs):
result = self.func(*args, **kwargs)
#-------------------------------
def __init__(self, func):
wraps(func)(self)
def __call__(self, *args, **kwargs):
result = self.__wrapped__(*args, **kwargs)
但這兩種方式都有缺陷,缺陷在於裝飾類中方法時。(注:在裝飾普通函數、類方法的時候,上面的方式不會出錯)
class cls:
@decorator
def method(self, x, y):...
因為self.func
和__wrapped__
裝飾cls中的方法時指向的都是cls的類變量(只不過這個屬性是裝飾器類decorator的實例對象而已),作為類變量,它無法保存cls的實例對象,也就是說method(self, x, y)
的self對裝飾器是不可見的。
用一個示例解釋更容易:
import types
from functools import wraps
# 作為裝飾器的類
class decorator:
def __init__(self,func):
self.func = func
def __call__(self, *args, **kwargs):
print("(1): ",self) # (1)
print("(2): ",self.func) # (2)
print("(3): ",args) # (3)
return self.func(*args, **kwargs)
class cls:
@decorator
def method(self, x, y):
return x + y
c = cls()
print("(4): ",c.method) # (4)
print(c.method(3, 4))
輸出結果:
(4): <__main__.decorator object at 0x03261630>
(1): <__main__.decorator object at 0x03261630>
(2): <function cls.method at 0x032C2738>
(3): (3, 4)
Traceback (most recent call last): File "g:/pycode/scope.py", line 21, in <module>
print(c.method(3, 4))
File "g:/pycode/scope.py", line 12, in __call__ return self.func(*args, **kwargs)
TypeError: method() missing 1 required positionalargument: 'y'
注意觀察上面__call__
中輸出的幾個對象:
- self對應的是decorator的實例對象method,而非cls的實例對象c,看輸出結果的前兩行即可知
- self.func指向的是原始方法method,它是類變量,是類方法(函數),是裝飾器賦予它作為函數的。也就是說,self.func指向的不是對象方法,而是類方法,類方法不會自動傳遞實例對象
- args中保存的參數列表是(3, 4),但是cls.method中多了一個self位置參數,使得3賦值給了self,4被賦值給了x,y成了多余的,所以最后報錯需要位置參數y。
如果將上面的method()的定義修改一下,把self去掉,將會正確執行:
class cls:
@decorator
def method(x, y):
return x + y
執行結果:
(4): <__main__.decorator object at 0x03151630>
(1): <__main__.decorator object at 0x03151630>
(2): <function cls.method at 0x031B2738>
(3): (3, 4)
7
因此參數問題必須解決。解決方案是進行判斷:如果是通過實例對象觸發的方法調用(即c.method()),就將外部函數通過types.MethodType()
鏈接到這個實例對象中,否則就返回原始self(因為它指向被裝飾的原始對象)。
這需要借助描述符來實現,關於這一段的解釋,我覺得直接看代碼自行腦部更佳。
class decorator:
def __init__(self,func):
wraps(func)(self)
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
# 如果不是通過對象來調用的
if instance is None:
return self
else:
return types.MethodType(self, instance)
class cls:
@decorator
def method(self, x, y):
return x + y
c = cls()
print(c.method(3, 4)) # 調用__get__后調用__call__
對於__wrapped__
也一樣可行:
class decorator():
def __init__(self, func):
wraps(func)(self)
def __call__(self, *args, **kwargs):
return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return types.MethodType(self, instance)
裝飾時是否帶參數
如果要讓作為裝飾器的類在裝飾時帶參數,就像函數裝飾器帶參一樣decorator(x,y,z)(func)
,可以將參數定義在__init__
上進行處理,然后在__call__
中封裝一層。
class Decorator:
def __init__(self, *args, **kwargs):
... do something with args ...
def __call__(self, func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
和函數裝飾器一樣,如果想要達到下面這種既能無參裝飾,又能帶參裝飾:
@Decorator # 無參裝飾
@Decorator(x,y,z) # 帶參裝飾
@Decorator() # 帶參裝飾,只不過沒給參數
可以直接在__init__
上進行參數有無的判斷:
import types
from functools import wraps, partial
class Decorator:
def __init__(self, func=None, arg1=1, arg2=2, arg3=3):
# 帶參裝飾器
if func is None:
self.func = partial(Decorator, arg1=arg1, arg2=arg2, arg3=arg3)
else: # 無參裝飾器
wraps(func)(self)
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return types.MethodType(self, instance)
這樣的限制是裝飾器如果帶參數時,必須使用keyword方式指定參數。例如:
# 帶參裝飾普通函數,使用keywords參數方式
@Decorator(arg1=1, arg2=3, arg3=5)
def func(x, y):
return x + y
print(func(11, 22))
print('-' * 30)
# 無參裝飾普通函數
@Decorator
def func1(x, y):
return x + y
print(func1(111, 22))
print('-' * 30)
# 無參裝飾方法
class cls:
@Decorator
def method(self, x, y):
return x + y
c = cls()
print(c.method(3, 4))
print('-' * 30)
# 帶參裝飾方法
class cls1:
@Decorator(arg1=1, arg2=3, arg3=5)
def method(self, x, y):
return x + y
cc = cls1()
print(cc.method(3, 4))
總結:類作為裝飾器的通用格式
如果不考慮裝飾時是否帶參數的問題,根據上面的一大堆分析,類作為裝飾器時的通用代碼格式如下:
import types
from functools import wraps
class Decorator:
def __init__(self, func):
wraps(func)(self)
# self.func = func
def __call__(self, *args, **kwargs):
# return self.func(*args, **kwargs)
# return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return types.MethodType(self, instance)
至於選擇self.func
的方式,還是self.__wrapped__
的方式,隨意。
如果需要考慮裝飾時帶參數問題,那么參考上一小節內容。
選擇類誰作為裝飾器?
函數可以作為裝飾器,類也可以作為裝飾器。它們也都能處理處理各種需求,但是類作為裝飾器的時候解釋了好大一堆,非常麻煩。所以,如果可以的話,選擇函數作為裝飾器一般會是最佳方案。