虽然人们能利用函数闭包(function clouser)写出简单的装饰器,但其可用范围常受限制。多数实现装饰器的基本方式会破坏与内省(Introspection)的关联性。
可大多数人会说:who cares!
但我仍坚持追求正确地写出漂亮代码。
我爱内省(introspection),讨厌猴子补丁(Monkey Patching)
请记住以下两点:
- 要为被装饰器包裹的函数(wrapped function)保留内省功能。
- 要理解清楚Python对象模型的执行方式如何工作。
接下来,我会通过14篇blog来向你解释:
- 你的典型Python装饰器及包裹的函数哪里有问题
- 如何修复这些问题
以下是第一篇内容,我会从几个方面简单说明你的典型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