9.9 将装饰器定义为类

问题

你想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。你需要让你的装饰器可以同时工作在类定义的内部和外部。

解决方案

为了将装饰器定义成一个实例,你需要确保它实现了 call()get() 方法。例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层:

  1. import types
  2. from functools import wraps
  3.  
  4. class Profiled:
  5. def __init__(self, func):
  6. wraps(func)(self)
  7. self.ncalls = 0
  8.  
  9. def __call__(self, *args, **kwargs):
  10. self.ncalls += 1
  11. return self.__wrapped__(*args, **kwargs)
  12.  
  13. def __get__(self, instance, cls):
  14. if instance is None:
  15. return self
  16. else:
  17. return types.MethodType(self, instance)

你可以将它当做一个普通的装饰器来使用,在类里面或外面都可以:

  1. @Profiled
    def add(x, y):
    return x + y

  2. class Spam:
    @Profiled
    def bar(self, x):
    print(self, x)

在交互环境中的使用示例:

  1. >>> add(2, 3)
  2. 5
  3. >>> add(4, 5)
  4. 9
  5. >>> add.ncalls
  6. 2
  7. >>> s = Spam()
  8. >>> s.bar(1)
  9. <__main__.Spam object at 0x10069e9d0> 1
  10. >>> s.bar(2)
  11. <__main__.Spam object at 0x10069e9d0> 2
  12. >>> s.bar(3)
  13. <__main__.Spam object at 0x10069e9d0> 3
  14. >>> Spam.bar.ncalls
  15. 3

讨论

将装饰器定义成类通常是很简单的。但是这里还是有一些细节需要解释下,特别是当你想将它作用在实例方法上的时候。

首先,使用 functools.wraps() 函数的作用跟之前还是一样,将被包装函数的元信息复制到可调用实例中去。

其次,通常很容易会忽视上面的 get() 方法。如果你忽略它,保持其他代码不变再次运行,你会发现当你去调用被装饰实例方法时出现很奇怪的问题。例如:

  1. >>> s = Spam()
  2. >>> s.bar(3)
  3. Traceback (most recent call last):
  4. ...
  5. TypeError: bar() missing 1 required positional argument: 'x'

出错原因是当方法函数在一个类中被查找时,它们的 get() 方法依据描述器协议被调用,在8.9小节已经讲述过描述器协议了。在这里,get() 的目的是创建一个绑定方法对象(最终会给这个方法传递self参数)。下面是一个例子来演示底层原理:

  1. >>> s = Spam()
  2. >>> def grok(self, x):
  3. ... pass
  4. ...
  5. >>> grok.__get__(s, Spam)
  6. <bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
  7. >>>

get() 方法是为了确保绑定方法对象能被正确的创建。type.MethodType() 手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。如果这个方法是在类上面来访问,那么 get() 中的instance参数会被设置成None并直接返回 Profiled 实例本身。这样的话我们就可以提取它的 ncalls 属性了。

如果你想避免一些混乱,也可以考虑另外一个使用闭包和 nonlocal 变量实现的装饰器,这个在9.5小节有讲到。例如:

  1. import types
  2. from functools import wraps
  3.  
  4. def profiled(func):
  5. ncalls = 0
  6. @wraps(func)
  7. def wrapper(*args, **kwargs):
  8. nonlocal ncalls
  9. ncalls += 1
  10. return func(*args, **kwargs)
  11. wrapper.ncalls = lambda: ncalls
  12. return wrapper
  13.  
  14. # Example
  15. @profiled
  16. def add(x, y):
  17. return x + y

这个方式跟之前的效果几乎一样,除了对于 ncalls 的访问现在是通过一个被绑定为属性的函数来实现,例如:

  1. >>> add(2, 3)
  2. 5
  3. >>> add(4, 5)
  4. 9
  5. >>> add.ncalls()
  6. 2
  7. >>>

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p09_define_decorators_as_classes.html