python重写和装饰器_Python装饰器系列02 - 装饰器和描述器之间的交互

上一篇博文中,我列出了传统Python装饰器所缺失的以下4项功能:保留函数的 __name__ and __doc__。

保留函数的参数定义。

保留获取函数源码的能力。

能够在带有描述器协议的其他装饰器上应用自己所写的装饰器。

上一篇章中,通过使用functools.wraps() ,

能够保留函数的__name__ 及__doc__属性。但是,它无法保留函数的参数定义,或保留获取函数源码的能力。

在本篇, 我们聚焦最后一项,关于装饰器与描述器的交互。把function wrapper应用于描述器(descriptor)。

何为描述器?

一般来说,一个描述器是一个有“绑定行为”的对象属性(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

>>> obj = object()>>> f.__get__(obj, type(obj))

>

当调用类方法(@classmethod)时,调用的并不是原函数对象的 __call__()方法,而是临时绑定对象的 __call__()方法。这个临时绑定对象在访问这个函数(即类方法)时所创建。

一般地,你不会看到前述的这些内部细节实现。>>> class Object(object):...   def f(self): pass

>>> obj = Object()>>> obj.f

>

回顾以下第一篇出现过的例子,当我们把装饰器放到类方法上时,会得到一个异常:class Class(object):    @function_wrapper    @classmethod

def cmethod(cls):

pass >>> Class.cmethod()

Traceback (most recent call last):

File "classmethod.py", line 15, in 

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),以免每次手工重复编写。本系列下篇文章将介绍如何实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值