手记

Python装饰器系列02 - 装饰器和描述器之间的交互

上一篇博文中,我列出了传统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样事情。

  1. __wrapped__储存wrapped function的引用。这是一个bug,它应该放在函数的最后部分实现。

  1. Copy __name____doc__等属性

  1. 把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




0人推荐
随时随地看视频
慕课网APP