这是关于Python装饰器系列文章的第二篇,第一篇在这里
如何正确地实现 Python 装饰器
上一篇博文中,我列出了传统Python装饰器所缺失的以下4项功能:
- 保留函数的
__name__
and__doc__
。 - 保留函数的参数定义。
- 保留获取函数源码的能力。
- 能够在带有描述器协议的其他装饰器上应用自己所写的装饰器。
上一篇章中,通过使用functools.wraps()
,
能够保留函数的__name__
及__doc__
属性。但是,它无法保留函数的参数定义,或保留获取函数源码的能力。
在本篇, 我们聚焦最后一项,关于装饰器与描述器的交互。把function wrapper应用于描述器(descriptor)。
何为描述器?
有关描述器的详细解释请参考Python描述器引导(翻译)
一般来说,一个描述器是一个有“绑定行为”的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__()
, __set__()
, 和 __delete__()
。有这些方法的对象叫做描述器。
-
obj.attribute
-->attribute.__get__(obj, type(obj))
-
obj.attribute = value
-->attribute.__set__(obj, value)
-
del obj.attribute
-->attribute.__delete__(obj)
如果一个类的属性拥有这些特殊方法,即可重写此属性的关联操作行为(取值/赋值/删除此属性)。
或许你认为从来不会用到描述器,但事实上函数对象就是描述器。当函数被添加到class定义时,它作为普通函数。当你通过.
号访问此函数时,你在调用__get__()
方法把此函数与实例绑定,让其成为对象的绑定方法。
def f(obj): pass
>>> hasattr(f, '__get__')
True
>>> f
<function f at 0x10e963cf8>
>>> obj = object()
>>> f.__get__(obj, type(obj))
<bound method object.f of <object object at 0x10e8ac0b0>>
当调用类方法(@classmethod
)时,调用的并不是原函数对象的 __call__()
方法,而是临时绑定对象的 __call__()
方法。这个临时绑定对象在访问这个函数(即类方法)时所创建。
一般地,你不会看到前述的这些内部细节实现。
>>> class Object(object):
... def f(self): pass
>>> obj = Object()
>>> obj.f
<bound method Object.f of <__main__.Object object at 0x10abf29d0>>
回顾以下第一篇出现过的例子,当我们把装饰器放到类方法上时,会得到一个异常:
class Class(object):
@function_wrapper
@classmethod
def cmethod(cls):
pass
>>> Class.cmethod()
Traceback (most recent call last):
File "classmethod.py", line 15, in <module>
Class.cmethod()
File "classmethod.py", line 6, in _wrapper
return wrapped(*args, **kwargs)
TypeError: 'classmethod' object is not callable
特别的是,人们所用的简单装饰器并没有实现描述器协议,将其应用在wrapped object(这里指classmethod)上时,会产生一个绑定的函数对象,理应这个函数对象会被调用。但实际上却是直接调用被包裹的对象,如果wrapped object没有__call__()
方法,便会导致异常抛出。
那为什么用于普通实例方法的装饰器可以正常工作呢?
因为普通函数仍具有__call__()
方法。在绕过被包裹函数的描述器协议后会继续调用__call__()
方法。且在调用原非绑定函数对象(original unbound function object)时,包装器(warpper)依然会显式地将self
作为第一个参数传给实例。
作为描述器的包装器
为解决上述讨论的问题,只需给包装器实现描述器协议。
class bound_function_wrapper:
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
class function_wrapper:
def __init__(self, wrapped):
self.wrapped = wrapped
def __get__(self, instance, owner):
wrapped = self.wrapped.__get__(instance, owner)
return bound_function_wrapper(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
当包装器应用在普通函数上时,其__call__()
方法会被调用。当包装器应用在类方法(@classmethod
)上时, __get__()
方法会被调用, __get__()
方法返回一个新绑定的包装器(bound wrapper),然后调用bound wrapper的__call__()
方法。通过描述器协议的传递,这个新的包装器就可以应用在描述器上了。
所以,用函数闭包去包裹带有描述器协议的装饰器会导致失败。若想让装饰器正常运作,我们应该以类方式去实现包装器,且这个类须实现描述器协议。
现在我们来厘清文章开头列出清单中的其他问题。
之前我们通过functools.wrap()
/functools.update_wrapper()
来解决名称(naming)问题,但是它们都做了些什么?我们还能继续使用它们吗?
好吧,wraps()
只是使用了 update_wrapper()
,我们来看看update_wrapper()
的实现。
WRAPPER_ASSIGNMENTS = ('__module__',
'__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper, wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
wrapper.__wrapped__ = wrapped
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(
getattr(wrapped, attr, {}))
上面是Python 3.3的代码,虽然当中有个在Python 3.4中已被修复的bug。:-)
函数的主体里完成了3样事情。
-
__wrapped__
储存wrapped function的引用。这是一个bug,它应该放在函数的最后部分实现。
- Copy
__name__
及__doc__
等属性
- 把wrapped function的
__dict__
Copy到包装器,当中包含了大部分需要Copy的内容。
当使用函数闭包或class形式的装饰器,这些copy动作会在套用装饰器时完成。
就算装饰器带有描述器协议,这些技术细节仍需在绑定包装器(bound wrapper)里完成。
class bound_function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
为了将函数绑定至class而调用包装器(wrapper)的时候,每次都会创建绑定包装器(bound wrapper)。这样会带来性能上的损失,我们需要额外的工作来解决这问题。
透明对象代理
解决性能问题的方案需要使用对象代理(object proxy),它是一个特殊的wrapper class,其外观及行为与被它包裹的对象相似。
class object_proxy(object):
def __init__(self, wrapped):
self.wrapped = wrapped
try:
self.__name__= wrapped.__name__
except AttributeError:
pass
@property
def __class__(self):
return self.wrapped.__class__
def __getattr__(self, name):
return getattr(self.wrapped, name)
一个完整的透明对象代理(A fully transparent object proxy)过于复杂, 这里带过细节不说,我会另撰文章解释。
上述例子简单地展现了它的工作方式。实践中它会做更多的工作,尤其是在使用猴子补丁(monkey patching)时。
总之,它从wrapped object上copy了有限的属性至自身,
使用了一些特殊方法、特性及__getattr__()
,当必要时才去获取被包裹对象的属性,这就避免了copy那些从不访问的属性。
现在我们只需利用对象代理来派生我们的包装器class,及移除update_wrapper()
class bound_function_wrapper(object_proxy):
def __init__(self, wrapped):
super(bound_function_wrapper, self).__init__(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
class function_wrapper(object_proxy):
def __init__(self, wrapped):
super(function_wrapper, self).__init__(wrapped)
def __get__(self, instance, owner):
wrapped = self.wrapped.__get__(instance, owner)
return bound_function_wrapper(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
这样,通过包装器(wrapper)查询__name__
和``doc`这些属性时,返回的是wrapped function的属性值,而不是包装器的属性值。
通过使用透明对象代理,inspect.getargspec()
和inspect.getsource()
现可如常运作了。无需额外工作即可同时解决了两个问题。
使这些更有用
上述方式虽解决了一开始提出的问题,但是它含有大量冗余的代码,包括两处重复调用wrapped function的地方。当你要为装饰器实现功能时,你仍要往这两处插入代码。
每次实现装饰器时重复这些劳动实在痛苦。
取而代之,我们需要把这些打包进装饰器工厂(decorator factory),以免每次手工重复编写。本系列下篇文章将介绍如何实现。
出处:https://github.com/GrahamDumpleton/wrapt/tree/develop/blog