Python装饰器系列01 - 如何正确地实现装饰器

虽然人们能利用函数闭包(function clouser)写出简单的装饰器,但其可用范围常受限制。多数实现装饰器的基本方式会破坏与内省(Introspection)的关联性。

可大多数人会说:who cares!

但我仍坚持追求正确地写出漂亮代码。

我爱内省(introspection),讨厌猴子补丁(Monkey Patching)

请记住以下两点:

  1. 要为被装饰器包裹的函数(wrapped function)保留内省功能。
  2. 要理解清楚Python对象模型的执行方式如何工作。

接下来,我会通过14篇blog来向你解释:

  1. 你的典型Python装饰器及包裹的函数哪里有问题
  2. 如何修复这些问题

以下是第一篇内容,我会从几个方面简单说明你的典型Python装饰器如何产生问题。


Python 装饰器基础知识

人皆所知Python装饰器语法如下:

@function_wrapper
def function():
    pass

@符号为自Python2.4引入的装饰器的语法糖(syntactic sugar), 它等同以下写法

def function():
    pass

function = function_wrapper(function)

@装饰器语法用于包裹定义或修改的函数

装饰器与猴子补丁不同,前者作用于定义时,后者作用于运行时


函数wrapper剖析

以下用class来实现一个装饰器

class function_wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
    def __call__(self, *args, **kwargs):
        return self.wrapped(*args, **kwargs)

@function_wrapper
def function():
    pass

以上例子,class实例初始化后会在其内部记录一个原函数(self.wrapped = wrapped),在调用这个被class装饰器包裹起来的函数时,实际上是通过调用class对象的__call()__方法来调用原函数。

你可以通过装饰器,在调用原函数之前或之后,实现一些额外的功能。如需修改传递给原函数的输入参数,或原函数返回的结果,你只要在__call__()方法内进行修改。

用class来实现装饰器或许不太流行(2014年)。普遍用函数闭包来实现装饰器。函数闭包实现方式为:利用嵌套函数逐层返回传入的原函数(wrapped)。代码如下:

def function_wrapper(wrapped):
    def _wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    return _wrapper 

@function_wrapper
def function():
    pass

此例中,无明显地给内嵌函数_wrapper传入原函数wrapped,内嵌函数仍可通过外层函数function_wrapper的参数访问到原函数(闭包原理),与用class实现装饰器相比,此做法方便多了。

Introspecting a function

函数内省

我们期望函数可指定一些与描述自身相关的特性(properties),如__name____doc__ 这样的属性。当我们把以函数闭包方式实现的装饰器应用到普通函数时,函数的这些属性会发生意料之外的变化。这些属性细节为内嵌函数提供。

def function_wrapper(wrapped):
    def _wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    return _wrapper 

@function_wrapper
def function():
    pass 

>>> print(function.__name__)
_wrapper

若以class方式实现的wrapper,类实例通常不带有__name__属性,以此方式去尝试访问原函数的name属性时会得到一个AttributeError异常

class function_wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
    def __call__(self, *args, **kwargs):
        return self.wrapped(*args, **kwargs) 

@function_wrapper
def function():
    pass 

>>> print(function.__name__)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function_wrapper' object has no attribute '__name__'

当以函数闭包方式实现装饰器時,为保留原函数相关信息,我们可以把原函数的相关属性Copy一份给内嵌函数。如下例,可正确获得原函数的__name____doc__内容。

def function_wrapper(wrapped):
    def _wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    _wrapper.__name__ = wrapped.__name__
    _wrapper.__doc__ = wrapped.__doc__
    return _wrapper 

@function_wrapper
def function():
    pass 

>>> print(function.__name__)
function

这样Copy属性实在费力,将来如有要追加的属性还得更新代码。例如我们想Copy__module__,还有Python 3新增加的__qualname____annotations__属性。我们可以利用Python标准库提供的functools.wraps()装饰器来实现这些需求。

import functools 

def function_wrapper(wrapped):
    @functools.wraps(wrapped)
    def _wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    return _wrapper 

@function_wrapper
def function():
    pass 

>>> print(function.__name__)
function

如以class方式实现装饰器,则可用functools.update_wrapper(),如下例所示:

import functools 

class function_wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
        functools.update_wrapper(self, wrapped)
    def __call__(self, *args, **kwargs):
        return self.wrapped(*args, **kwargs)

虽然functools.wraps()能解决诸如访问原函数的__name____doc__的问题,但实际上并没有完美解决函数内省,接下来你会看到。

当我们查询被装饰器包裹的原函数的参数定义时,返回的结果却是wrapper的参数定义。以函数闭包实现的装饰器为例,返回的为内嵌函数的参数定义。因此,装饰器不具签名保护(not signature preserving)

import inspect 

def function_wrapper(wrapped): 
    def _wrapper(*arg, **kwarg):
        return wrapped(*arg, **kwarg)
    return _wrapper

@function_wrapper
def function(arg1, arg2): pass 

>>> print(inspect.signature(function))
(*arg, **kwarg)

以class实现的装饰器也是同样的结果。

import inspect

class function_wrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __call__(self, *arg, **kwarg):
        return self.wrapped(*arg, **kwarg)

@function_wrapper
def function(arg1, arg2): pass 

>>> print(inspect.signature(function))
(*arg, **kwarg)

另一个和内省相关的例子是,当用inspect.getsource()尝试返回函数(此函数被以class方式实现的装饰器包裹起来)的源码时,会得到一个TypeError异常。

TypeError: <__main__.function_wrapper object at 0x0000020B2AD6C828> is not a module, class, method,
 function, traceback, frame, or code object
The terminal process terminated with exit code: 1

包裹class方法

和普通函数一样,装饰器也可应用在class的方法上。Python内置的两个特殊装饰器——@staticmethod@classmethod可将普通的实例方法(instance method)转化为class相关的特殊方法。虽然这些特殊方法也隐含着一些问题。

class Class(object): 

    @function_wrapper
    def method(self):
        pass 

    @classmethod
    def cmethod(cls):
        pass 

    @staticmethod
    def smethod():
        pass

首先,就算在你的装饰器里用上了 functools.wraps()functools.update_wrapper(),当你把这个装饰器放在 @classmethod@staticmethod前面时,依然会得到一个异常。这是因为依然有一些属性并未被functools.wraps()functools.update_wrapper()Copy进来。以下为Python2的运行情况。

class Class(object):
    @function_wrapper
    @classmethod
    def cmethod(cls):
        pass 

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in Class
  File "<stdin>", line 2, in wrapper
  File ".../functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'classmethod' object has no attribute '__module__'

此为Python2的bug所致,此bug已在Python3中得到修正。

就算在Python3中运行,依然有异常抛出。那是因为两个包裹类型(wrapper types,即@function_wrapper@classmethod)都期望被包裹函数(wrapped function)是可以被直接调用的(callable)。此被包裹的函数可称之为描述器(descriptor)。这意味为了返回一个可调用的描述器,它(描述器)须先正确地与实例绑定起来。参考以下代码

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

简单并非意味着正确

虽然我们可以简单地实现装饰器,并不见得这些装饰器必然正确及长久有效。

至此,比较突出的问题如下:

  • 保留函数的 __name__ and __doc__
  • 保留函数的参数定义。
  • 保留获取函数源码的能力。
  • 能够在带有描述器协议的其他装饰器上应用自己所写的装饰器。

functools.wraps() 为我们解决了第一个问题,但不能一劳永逸。例如不能解决内省相关的问题。

就算能解决内省相关的问题,简单实现的装饰器依然会破坏python对象的执行模型,譬如被装饰器包裹着的带描述器协议的对象。

第三方包(packages)如decorator模块尝试解决这些问题,但只能解决前面两点问题。通用猴子补丁动态地应用函数包装器(function wrapper)时依然会发生问题。

我们找出了一些问题,后续博文中,我们会看到如何解决这些问题。而且你也会写出优雅的装饰器。

请继续关注我下期博文,希望我能保持继续写博的冲劲。

出处:https://github.com/GrahamDumpleton/wrapt/tree/develop/blog

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

推荐阅读更多精彩内容

  • 这是关于Python装饰器系列文章的第二篇,第一篇在这里如何正确地实现 Python 装饰器 上一篇博文中,我列出...
    gomibako阅读 614评论 0 2
  • 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,...
    chen_000阅读 1,360评论 0 3
  • 有时候我们想为多个函数,同意添加某一种功能,比如及时统计,记录日志,缓存运算结果等等,而又不想改变函数代码那就定义...
    ketchup阅读 3,041评论 0 3
  • 有关宪法的通常定义 “宪法”一词,来源于拉丁文constitution,本是组织、确立的意思。 古罗马帝国用它来表...
    駟馬華輦阅读 432评论 0 3
  • 距离开片,已近一个小时,看到喜极而泣的母子相拥那一刻,我没忍住,发出轻微一声冷笑。 这诡异的一笑,像是划破了四下无...
    三度七月阅读 1,014评论 11 29