手记

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

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

可大多数人会说:who cares!

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

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

请记住以下两点:

  1. 要为被装饰器包裹的函数(wrapped function)保留内省功能。

  2. 要理解清楚Python对象模型的执行方式如何工作。

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

  1. 你的典型Python装饰器及包裹的函数哪里有问题

  2. 如何修复这些问题

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


Python 装饰器基础知识

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

@function_wrapperdef function():
    pass

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

def function():
    passfunction = 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_wrapperdef 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_wrapperdef 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_wrapperdef 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_wrapperdef 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_wrapperdef 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_wrapperdef 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_wrapperdef function(arg1, arg2): pass >>> print(inspect.signature(function))
(*arg, **kwarg)

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

import inspectclass function_wrapper:
    def __init__(self, wrapped):        self.wrapped = wrapped    def __call__(self, *arg, **kwarg):        return self.wrapped(*arg, **kwarg)

@function_wrapperdef 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 objectThe 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



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