Decorators

改变一个函数的行为另一种常见的方式是用另一个函数来装饰(decorate)它。这也经常称之为“包装”一个函数,因为装饰器是被设计成在要调用原生函数之前或之后运行附加的代码。

装饰器背后的关键原则是他们接受可调用的内容,返回一个新的可调用的内容。由装饰器返回的函数是在被装饰的函数被调用时执行。要注意的是,确保原生函数在处理过程不遗漏,因为没有办法在没有重载模块的情况下get it back。

装 饰器可以采用多个方法,或者应用在你直接定义的函数上,或是其他地方定义的函数。到python 2.4时,对一个新定义的函数,装饰器可以使用一个特殊的语法。之前的python版本,有些稍微不同的语法,但是这两种情况,代码(的效果)是一样的。 不同的地方就是作用于函数上的装饰器的语法。

>>> def decorate(func):
...     print 'Decorating %s...' % func.__name__,
...     def wrapped(*args, **kwargs):
...         print "Called wrapped function with args:", args
...         return func(*args, **kwargs)
...     print 'done!'
...     return wrapped
...

#Syntax for python 2.4 and higher

>>> @decorate
... def test(a, b):
...     return a + b
...
Decorating test... done!
>>> test(13, 72)
Called wrapped function with args: (13, 72)
85
>>> #Syntax for python 2.3
...
>>> def test(a, b):
...     return a + b
...
>>> test = decorate(test)
Decorating test... done!
>>> test(13, 72)
Called wrapped function with args: (13, 72)
85

这 个例子中的老式语法是装饰函数的另一种技巧,可以用在那些@不能用的地方。适用于一个函数在其他地方已经声明过了,而想利用一下装饰器。这样一个函数就能 被传入给一个装饰器了,然后返回一个新函数,所有的东西都在里面包装起来了。使用这个技巧,任何可调用的东西,不管来自哪里或作什么,都能被任何装饰器包 装。

使用过量参数装饰

有时,一个装饰器需要一些附加的信息来决定应该怎么处理接受的函数。使用老式的装饰语法,或装饰任何函数,这个任务都是很容易处理的。简单的声明装饰器,接受附加的参数,要求的信息就能配置到要包装的函数中去。

>>> def decorate(func, prefix='Decorated'):
...     def wrapped(*args, **kwargs):
...         return '%s: %s' % (prefix, func(*args, **kwargs))
...     return wrapped
...
>>> simple = decorate(test)
>>> customized = decorate(test, prefix='Custom')
>>> simple(30, 5)
Called wrapped function with args: (30, 5)
'Decorated: 35'
>>> customized(27, 15)
Called wrapped function with args: (27, 15)
'Custom: 42'

然而,python 2.4的装饰器语法就麻烦了。当使用新式语法的时候,装饰器总是只接受一个参数:被包装的函数。有一个方法把附加的参数放入装饰器中,但是我们要先停一下,先讨论一下"partials"。

函数的partial应用

典型的,函数在执行时,要带上所有必要的参数进行调用。然后,有时参数可以在函数被调用之前提前获知。这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。

出于这个目的,python 2.5包括了partiall对象,作为functools模块的一部分。它接受一个可调用的(),可以包含任意个数附加参数,返回一个新的可调用的(),行为就像原生的一样,只是不要指定那些预加载的参数。

>>> import functools
>>> def add(a, b):
...     return a + b
...
>>> add(4, 2)
6
>>> plus3 = functools.partial(add, 3)
>>> plus5 = functools.partial(add, 5)
>>> plus3(4)
7
>>> plus3(7)
10
>>> plus5(10)
15

对于python 2.5之前的版本,django提供了它自己partial实现,curry函数,在django.utils.functional模块中。这个函数可用在python 2.3和之后的版本。

返回装饰器的遗留问题

前 面提到了,装饰器使用python 2.4的语法,在接受附加参数时,会有一个问题,因为语法上只能对函数提供单一的参数。使用partial技巧后,就有可能在一个装饰器上预先加载参数。 前面给的装饰器,下面用curry(第9章有详细说明)来提供给采用python 2.4语法的装饰器参数。

>>> from django.utils.functional import curry
>>> @curry(decorate, prefix='Curried')
... def test(a, b):
...     return a + b
...
>>> test(30, 5)
'Curried: 35'
>>> test(27, 15)
'Curried: 42'
>>>

这还是相当不方便,因为每次用来装饰另一个函数时函数需要通过curry来运行。更好的方法是把这个函数直接装配到装饰器自身里。这当然在装饰器上要写更多的代码,但是包含这些代码使得用起来容易一些。

这个技巧是在另一个函数中定义一个装饰器,它接受参数的。这个新的函数,返回的是装饰器,然后它可以由python标准装饰器处理。反过来,这个装饰器返回的函数,是在装饰之后,有剩余的代码来使用。

说起来相当抽象,考虑下面的代码,它提供了前面例子相同的功能,但是不依赖curry,处理起来很容易。

>>> def decorate(prefix='Decorated'):
...     # The prefix passed in here will be
...     # available t all the inner functions
...     def decorator(func):
...         # This is called with func being the
...         # actual function being decorated
...         def wrapper(*args, **kwargs):
...             # This will be called each time
...             # the real function is excuted
...             return '%s: %s' % (prefix, func(*args, **kwargs))
...         # Send the wrapped function
...         return wrapper
...     # Provide the decorator for Python to use
...     return decorator
...
>>> @decorate('Easy')
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
'Easy: 30'
>>> test(89, 121)
'Easy: 210'

这个技巧对于已知的参数(arguments are expected)非常有意义。如果装饰器用在没有任何参数的地方,为了正常使用括号是必备的。

>>> @decorate()
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
'Decorated: 30'
>>> test(89, 121)
'Decorated: 210'
>>> @decorate
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: decorator() takes exactly 1 argument (2 given)

第2个例子失败是因为我们没有先调用decorate。因此,所有随后对test的调用把参数都发送给了decorator,而不是test。因为没有匹配成功,python就抛出了一个错误。这种情况调试起来有点困难,因为抛出的异常是依赖于被包装的函数。

带或不带参数的装饰器

装饰器的另一个选择是只提供单一的装饰器作用于前面的两个例子:带参数和不带参数,这有点复杂,但值得探索。

这 样作的目标是允许装饰器带参数或不带参数调用,因此假定所有的参数都是可选的;任何要求参数的装饰器不能使用这个技巧。记住,一个基本的原则,在列表的开 头添加一个附加的可选参数,它是被装饰的函数。然后就是装饰器的代码结构上包括必要的逻辑来判断它是否增加参数来调用,或是否装饰目标函数。

>>> def decorate(func=None, prefix='Decorated'):
...     def  decorated(func):
...         # This returns the final, decorated
...         # function, regardless of how it was called
...         def wrapper(*args, **kwargs):
...             return '%s: %s' % (prefix, func(*args, **kwargs))
...         return wrapper
...     if func is None:
...         # The decorator was called with arguments
...         def decorator(func):
...             return decorated(func)
...         return decorator
...     # The decorator was called without arguments
...     return decorated(func)
...
>>> @decorate
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
'Decorated: 30'
>>> @decorate(prefix='Arguments')
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
'Arguments: 30'

这要求所有传递给装饰器的参数作为一个关键字参数来传递,通常这也有增加了代码的可读性。一个不足就是采用这个方法对每个装饰器来说,许多“boilerplate”不断要重复。

像python里大多数"boilerplate",可能可以提炼出公共部分作为复用的形式,因此新的装饰器更容易定义,作用于另一个装饰器。下面的函数就是被用来装饰其他的函数,接受参数或不带参数从而提供所有必要的功能。

>>> def optional_arguments_decorator(real_decorator):
...     def decorator(func=None, **kwargs):
...         # This is the decorator that will be
...         # exposed to the rest of your program
...         def decorated(func):
...             # This returns the final, decorated
...             # function, regardless of hwo it was called
...             def wrapper(*a, **kw):
...                 return real_decorator(func, a, kw, **kwargs)
...             return wrapper
...         if func is None:
...             # The decorator was called with arguments
...             def decorator(func):
...                 return decorated(func)
...             return decorator
...         # The decorator was called without arguments
...         return decorated(func)
...     return decorator
...
>>> @optional_arguments_decorator
... def decorate(func, args, kwargs, prefix='Decorated'):
...     return '%s: %s' % (prefix, func(*args, **kwargs))
...
>>> @decorate
... def test(a, b):
...     return a + b
...
>>> test(13, 17)
'Decorated: 30'
>>> test = decorate(test, prefix='Decorated again')
>>> test(13, 17)
'Decorated again: Decorated: 30'

这个例子使得装饰器的定义更简单,更直接。这种装饰器的结果和前面的例子行为一样,但是它能带或者不带参数。最值得注意的变化就是新的技巧,被定义的真正的装饰器,接受的是下面3个值:
1)func -- 使用新生成的装饰器产生的被装饰的函数
2)args -- 传递给函数的包含位置参数的元组
3)kwargs -- 传递给函数的包含关键字参数的字典

然后,有一个重要的方面,args和kwargs,这两个装饰器接受的,作为位置参数来传递,并没有通常用的星号。然后,当把它们传递给被包装的函数,星号必须带上,确保函数正确接受,而不用知道装饰器是如何工作的。