离线下载
PDF版 ePub版

qyuhen · 更新于 2018-11-28 11:00:43

装饰器

装饰器 (Decorator) 在 Python 编程中极为常见,可轻松实现 Metadata、Proxy、 AOP 等模式。简单点说,装饰器通过返回包装对象实现间接调用,以此来插入额外逻辑。

语法看上去和 Java Annotation、C# Attribute 类似,但不仅仅是添加元数据。

>>> @check_args
... def test(*args):
... print args

还原成容易理解的方式:

>>> test = check_args(test)

类似的做法,我们在使用 staticmethod、classmethod 时就已见过。

>>> def check_args(func):
...     def wrap(*args):
...         args = filter(bool, args)
...         func(*args)
...
...     return wrap   # 返回 wrap 函数对象

>>> @check_args    # 解释器执行 test = check_args(test)
... def test(*args):
...     print args

>>> test     # 现在 test 名字与 wrap 关联。
<function wrap at 0x108affde8>

>>> test(1, 0, 2, "", [], 3)  # 通过 wrap(test(args)) 完成调用。
(1, 2, 3)

整个过程非常简单:

  • 将目标函数对象 test 作为参数传递给装饰器 check_args。
  • 装饰器返回包装函数 wrap 实现对 test 的间接调用。
  • 原函数名字 test 被重新关联到 wrap,所有对该名字的调用实际都是调用 wrap。

你完全可以把 "@" 当做语法糖,也可以直接使用函数式写法。只不过那样不便于代码维护,毕竟 AOP 极力避免代码侵入。

装饰器不一定非得是个函数返回包装对象,也可以是个类,通过 call 完成目标调用。

>>> class CheckArgs(object):
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args):
...         args = filter(bool, args)
...         self._func(*args)

>>> @CheckArgs      # 生成 CheckArgs 实例。
... def test(*args):
...     print args

>>> test       # 名字指向该实例。
<__main__.CheckArgs object at 0x107a237d0>

>>> test(1, 0, 2, "", [], 3)    # 每次都是通过该实例的 __call__ 调用。
(1, 2, 3)

用类装饰器对象实例替代原函数,以后的每次调用的都是该实例的 call 方法。这种写法有点啰嗦,还得注意避免在装饰器对象上保留状态。

Class

为 Class 提供装饰器同样简单,无非是将类型对象做为参数而已。

>>> def singleton(cls):
...     def wrap(*args, **kwargs):
...         o = getattr(cls, "__instance__", None)
...         if not o:
...             o = cls(*args, **kwargs)
...             cls.__instance__ = o
...
...         return o
...
...     return wrap   # 返回 wrap 函数,可以看做原 class 的工厂方法。

>>> @singleton
... class A(object):
...     def __init__(self, x):
...         self.x = x

>>> A
<function wrap at 0x108afff50>

>>> a, b = A(1), A(2)
>>> a is b
True

将 class A 替换成 func wrap 可能有些不好看,修改一下,返回 class wrap。

>>> def singleton(cls):
...     class wrap(cls):
...         def __new__(cls, *args, **kwargs):
...             o = getattr(cls, "__instance__", None)
...             if not o:
...                 o = object.__new__(cls)
...                 cls.__instance__ = o
...
...             return o
...
...     return wrap

>>> @singleton
... class A(object):
...     def test(self): print hex(id(self))

>>> a, b = A(), A()

>>> a is b
True

>>> a.test()
0x1091e9990

创建继承自原类型的 class wrap,然后在 new 里面做手脚就行了。

大多数时候,我们仅用装饰器为原类型增加一些额外成员,那么可直接返回原类型。

>>> def action(cls):
...     cls.mvc = staticmethod(lambda: "Action")
...     return cls

>>> @action
... class Login(object): pass

>>> Login.mvc()
'Action'

这就是典型的 metaprogramming 做法了。

参数

参数让装饰器拥有变化,也更加灵活。只是需要两步才能完成:先传参数,后送类型。

>>> def table(name):
...     def _table(cls):
...         cls.__table__ = name
...         return cls
...
...     return _table

>>> @table("t_user")
... class User(object): pass

>>> @table("t_blog")
... class Blog(object): pass

>>> User.__table__
't_user'

>>> Blog.__table__
't_blog'

只比无参数版本多了传递参数的调用,其他完全相同。

User = (table("t_user"))(User)

嵌套

可以在同一目标上使用多个装饰器。

>>> def A(func):
...     print "A"
...     return func

>>> def B(func):
...     print "B"
...     return func

>>> @A
... @B
... def test():
...     print "test"

B
A

分解一下,无非是函数嵌套调用。

test = A(B(test))

functools.wraps

如果装饰器返回的是包装对象,那么有些东西必然是不同的。

>>> def check_args(func):
...     def wrap(*args):
...         return func(*filter(bool, args))
...
...     return wrap

>>> @check_args
def test(*args):
...     """test function"""
...     print args

>>> test.__name__   # 冒牌货!
'wrap'

>>> test.__doc__   # 山寨货连个说明书都木有!

一旦 test 的调用者要检查某些特殊属性,那么这个 wrap 就会暴露了。幸好有 functools.wraps。

>>> def check_args(func):
...     @functools.wraps(func)
...     def wrap(*args):
...         return func(*filter(bool, args))
...
...     return wrap

>>> @check_args
def test(*args):
    """test function"""
    print args

>>> test
<function test at 0x108b026e0>

>>> test.__name__
'test'

>>> test.__doc__
'test function'

>>> test(1, 0, 2, "", 3)
(1, 2, 3)

functools.wraps 是装饰器的装饰器,它的作用是将原函数对象的指定属性复制给包装函数对象,默认有 modulenamedoc,或者通过参数选择。

想想看装饰器都能干嘛?

  • AOP: 身份验证、参数检查、异常日志等等。
  • Proxy: 对目标函数注入权限管理等。
  • Context: 提供函数级别的上下文环境,比如 Synchronized(func) 同步。
  • Caching: 先检查缓存是否过期,然后再决定是否调用目标函数。
  • Metaprogramming: 这个自不必多说了。
  • 等等……
上一篇: 异常 下一篇: 描述符