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

这是关于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样事情。

  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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 虽然人们能利用函数闭包(function clouser)写出简单的装饰器,但其可用范围常受限制。多数实现装饰器的...
    gomibako阅读 1,010评论 0 4
  • 装饰是为函数和类指定管理代码的一种方式.装饰器本身的形式是处理其他的可调用对象的可调用的对象。 函数装饰器在函数定...
    低吟浅唱1990阅读 222评论 0 0
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 我喜欢自己乡村的傍晚,傍晚时屋顶上空悠然升起的炊烟。烟囱下灶塘弥漫着食物的清香,那欢声笑语的屋子座落在傍晚的村庄中...
    长林张少阅读 1,648评论 0 7