装饰器 1: 函数装饰器
装饰器 2: 类装饰器
装饰器 3: 进阶
函数装饰器装饰方法
函数装饰器装饰普通函数已经很容易理解了:
- @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__的方式, 随意.
如果需要考虑装饰时带参数问题, 那么参考上一小节内容.
选择类谁作为装饰器?
函数可以作为装饰器, 类也可以作为装饰器. 它们也都能处理处理各种需求, 但是类作为装饰器的时候解释了好大一堆, 非常麻烦. 所以, 如果可以的话, 选择函数作为装饰器一般会是最佳方案.
来源: https://www.cnblogs.com/f-ck-need-u/p/10204617.html