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

虽然人们能利用函数闭包(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 "", line 1, in

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> 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 "", line 1, in

File "", line 3, in Class

File "", 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

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)时依然会发生问题。

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

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值