创建装饰器时保留函数元信息
问题
你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。
解决方案
任何时候你定义装饰器的时候,都应该使用functools 库
中的 @wraps
装饰器来注解底层包装函数。例如:
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@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
使用这个被包装后的函数并检查它的元信息:
>>> @timethis
... def countdown(n):
... '''
... Counts down
... '''
... while n > 0:
... n -= 1
...
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown.__name__
'countdown'
>>> countdown.__doc__
'\n\tCounts down\n\t'
>>> countdown.__annotations__
{'n': <class 'int'>}
>>>
讨论
在编写装饰器的时候复制元信息是一个非常重要的部分。如果你忘记了使用@wraps
, 那么你会发现被装饰函数丢失了所有有用的信息。比如如果忽略@wraps
后的效果是下面这样的:
>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}
>>>
@wraps
有一个重要特征是它能让你通过属性 __wrapped__
直接访问被包装函数。例如:
>>> countdown.__wrapped__(100000)
>>>
__wrapped__
属性还能让被装饰函数正确暴露底层的参数签名信息。例如:
>>> from inspect import signature
>>> print(signature(countdown))
(n:int)
>>>
一个很普遍的问题是怎样让装饰器去直接复制原始函数的参数签名信息, 如果想自己手动实现的话需要做大量的工作,最好就简单的使用 @wraps
装饰器。 通过底层的 __wrapped__
属性访问到函数签名信息。更多关于签名的内容可以参考9.16小节。
9.8 将装饰器定义为类的一部分
问题
你想在类中定义装饰器,并将其作用在其他函数或方法上。
解决方案
在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法
还是类方法
。 下面我们用例子来阐述它们的不同:
from functools import wraps
class A:
# Decorator as an instance method
def decorate1(self,func):
@wraps(func)
def wrapper(*args,**kwargs):
print('Decorate 1')
return func(*args,**kwargs)
return wrapper
# Decorator as a class method
@classmethod
def decorate2(cls,func):
@wraps(func)
def wrapper(*args,**kwargs):
print('Decorate 2')
return func(*args,**kwargs)
return wrapper
# As an instance method
a=A()
@a.decorate1
def spam():
pass
# As a class method
@A.decorate2
def grok():
pass
仔细观察可以发现一个是实例调用,一个是类调用。
讨论
在类中定义装饰器初看上去好像很奇怪,但是在标准库中有很多这样的例子。 特别的,@property
装饰器实际上是一个类,它里面定义了三个方法 getter()
, setter()
, deleter()
, 每一个方法都是一个装饰器。例如:
class Person:
# Create a property instance
first_name = property()
# Apply decorator methods
@first_name.getter
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
它为什么要这么定义的主要原因是各种不同的装饰器方法会在关联的 property
实例上操作它的状态。 因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法。
在类中定义装饰器有个难理解的地方就是对于额外参数self
或 cls
的正确使用。 尽管最外层的装饰器函数比如 decorator1() 或 decorator2() 需要提供一个 self
或 cls
参数, 但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个self
参数。 你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。
对于类里面定义的包装器还有一点比较难理解,就是在涉及到继承的时候。 例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:
class B(A):
@A.decorator2
def bar(self):
pass
也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。 你不能使用 @B.decorator2
,因为在方法定义时,这个类B还没有被创建。
9.9 将装饰器定义为类
问题
你想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。 你需要让你的装饰器可以同时工作在类定义的内部和外部。
解决方案
为了将装饰器定义成一个实例,你需要确保它实现了 __call__()
和 __get__()
方法。 例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层:
import types
from functools import wraps
class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)
你可以将它当做一个普通的装饰器来使用,在类里面或外面都可以:
@Profiled
def add(x, y):
return x + y
class Spam:
@Profiled
def bar(self, x):
print(self, x)
>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls
2
>>> s = Spam()
>>> s.bar(1)
<__main__.Spam object at 0x10069e9d0> 1
>>> s.bar(2)
<__main__.Spam object at 0x10069e9d0> 2
>>> s.bar(3)
<__main__.Spam object at 0x10069e9d0> 3
>>> Spam.bar.ncalls
3
讨论
将装饰器定义成类通常是很简单的。但是这里还是有一些细节需要解释下,特别是当你想将它作用在实例方法上的时候。
首先,使用functools.wraps()
函数的作用跟之前还是一样,将被包装函数的元信息复制到可调用实例中去。
其次,通常很容易会忽视上面的__get__()
方法。如果你忽略它,保持其他代码不变再次运行, 你会发现当你去调用被装饰实例方法时出现很奇怪的问题。例如:
>>> s = Spam()
>>> s.bar(3)
Traceback (most recent call last):
...
TypeError: bar() missing 1 required positional argument: 'x'
出错原因是当方法函数在一个类中被查找时,它们的 __get__()
方法依据描述器协议被调用, 在8.9小节已经讲述过描述器协议了。在这里,__get__()
的目的是创建一个绑定方法对象 (最终会给这个方法传递self参数)。下面是一个例子来演示底层原理:
>>> s = Spam()
>>> def grok(self, x):
... pass
...
>>> grok.__get__(s, Spam)
<bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
>>>
__get__()
方法是为了确保绑定方法对象能被正确的创建。 type.MethodType()
手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。 如果这个方法是在类上面来访问, 那么__get__()
中的instance参数会被设置成None并直接返回 Profiled
实例本身。 这样的话我们就可以提取它的 ncalls
属性了。
为类和静态方法提供装饰器
问题
你想给类或静态方法提供装饰器。
解决方案
给类或静态方法提供装饰器是很简单的,不过要确保装饰器在 @classmethod
或@staticmethod
之后。例如:
import time
from functools import wraps
# A simple decorator
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print(end-start)
return r
return wrapper
# Class illustrating application of the decorator to different kinds of methods
class Spam:
@timethis
def instance_method(self, n):
print(self, n)
while n > 0:
n -= 1
@classmethod
@timethis
def class_method(cls, n):
print(cls, n)
while n > 0:
n -= 1
@staticmethod
@timethis
def static_method(n):
print(n)
while n > 0:
n -= 1
装饰后的类和静态方法可正常工作,只不过增加了额外的计时功能:
>>> s = Spam()
>>> s.instance_method(1000000)
<__main__.Spam object at 0x1006a6050> 1000000
0.11817407608032227
>>> Spam.class_method(1000000)
<class '__main__.Spam'> 1000000
0.11334395408630371
>>> Spam.static_method(1000000)
1000000
0.11740279197692871
>>>
讨论
如果你把装饰器的顺序写错了就会出错。例如,假设你像下面这样写:
class Spam:
@timethis
@staticmethod
def static_method(n):
print(n)
while n > 0:
n -= 1
那么你调用这个静态方法时就会报错:
>>> Spam.static_method(1000000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "timethis.py", line 6, in wrapper
start = time.time()
TypeError: 'staticmethod' object is not callable
>>>
问题在于 @classmethod
和 @staticmethod
实际上并不会创建可直接调用的对象, 而是创建特殊的描述器对象(参考8.9小节)。因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。 确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题。