python装饰器2:进阶
本文是装饰器相关内容的第二篇,接上一篇。
函数装饰器装饰方法
函数装饰器装饰普通函数已经很容易理解了:
@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__
的方式,随意。
如果需要考虑装饰时带参数问题,那么参考上一小节内容。
选择类谁作为装饰器?
函数可以作为装饰器,类也可以作为装饰器。它们也都能处理处理各种需求,但是类作为装饰器的时候解释了好大一堆,非常麻烦。所以,如果可以的话,选择函数作为装饰器一般会是最佳方案。
上一篇: 设计模式之空对象模式