Python Decorator 装饰器

何为装饰器?


装饰器(decorator)是一种语法糖,需要编程语言支持闭包和first-class函数闭包(提供局部变量支持,first-class函数允许将函数作为参数和任意地方创建函数)。装饰器简单来说就是装饰一个函数(对象),在调用之前或者之后附加点操作逻辑,类似下面的逻辑。

1
2
3
4
5
6
7
8
9
def do_something():
#do_something
==>
def do_something():
pre_do_something #装饰器附加操作
do_something
post_do_somthding #装饰器附件操作

如果没有装饰器语法如何实现相同功能?


如果没有装饰器语法的支持,我们可以用现有的语法实现。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def wrapped(arg):
print arg
wrapped = decorator(wrapped)
wrapped('hello,world')
>>> hello,world
wrapped.__name__
>>>'wrapper'

上面的逻辑比较简单。以wrapped为参数调用decorator,返回一个wrapper函数,再赋值给wrapped名字。表面看wrapped没有改变,其实际内容已经被替换成wrapper这个闭包。有了装饰器语法糖的支持,我们可以简写成:

1
2
3
@decorator
def wrapped(arg):
print arg

python解释器负责调用decorator(wrapped)再赋值给wrapped名字的过程。当然上面的例子比较简单,并没有做一些额外的工作,可以稍微修改一下。

1
2
3
4
5
6
7
8
def decorator(func)
def wrapper(*args, **kwarg):
args = tuple(s.title() for s in args)
return func(*args, **kwargs)
return wrapper
wrapped('hello,world')
>>>'Hello,world'

函数如何使用装饰器?


上面的例子已经给出如何使用装饰器的方式,即在被装饰函数的前面加上@decorator

1
2
3
4
5
6
7
8
9
10
11
def decorator(func):
def wrapper(name):
return func(' '.join(name, 'are a dog'))
return wrapper
@decorator
def echo_name(name):
print name
echo_name('you')
>> 'you are a dog'

如何使用多装饰器?


python解释器负责从底向上调用装饰器赋值,因此可以写为

1
2
3
4
5
@decorator_2
@decorator_1
@decorator_0
def func():
pass

即按照装饰的顺序由下向上调用。类似func = decorator_2(decorator_1(decorator_0(func)))

如何给装饰器转入参数


从前面看,装饰器要求最后能够返回一个函数。这个函数接受唯一的参数func即可。因此实现装饰器传入参数有几种办法。第一种,多层嵌套,装饰器本身接受参数后,再返回一个装饰器即可。当然,为了利用转入装饰器的参数,需要把参数再继续传递下去。

1
2
3
4
5
6
7
def decorator_outer(arg):
def decorator_inner(func, arg=arg):
def inner(*args, **kwargs):
args = args + (arg,)
return func(*args, **kwargs)
return inner
return decorator_inner

第二种,利用类实现参数。

1
2
3
4
5
6
7
8
9
10
class decorator(object):
def __init__(self, arg):
self.arg = arg
def inner(self, *args, **kwargs):
return self.func(*args, **kwarg)
def __call__(self, func):
self.func = func
return self.inner

利用类实现实际上是手工编写了一个闭包,再利用__call__魔术方法将类当做函数运行。

第三种,利用functools.partial 将嵌套扁平化。

1
2
3
4
5
6
7
import functools
def decorator(d_arg)
def inner(func, *args, **kwargs):
args = args + kwargs.get('d_arg', ())
return func(*args, **kwargs)
return functools.partial(inner, d_arg=d_arg)

partial叫做函数的柯西化,即将多参数分阶段传入,等待传入最后一个参数才运行的技术。

1
2
3
4
5
6
7
8
def func(a, b, c):
print a,b,c
func = partial(func, a=1)
func = partial(func, b=2)
func = partial(func, c=3)
func()
>>>1, 2, 3

装饰器的优点?


可以不改变函数名称和参数的情况下实现额外的功能。因为可以在不改变原有逻辑和代码的情况下附加功能,可以实现相同前置/后置逻辑的抽象。

python本身利用装饰器语法实现了很多功能。例如: @classmethod 实现类方法,@staticmethod实现类的静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Klass(object):
@classmethod
def get(cls):
print 'classmethod'
@staticmethod:
def echo():
print 'staticmethod'
@property
def value(self):
if self._value < 1:
return 0
else:
return self._value

还有例如用 @property 装饰器将类方法转成类属性(如上所示)等。编程中可以将所有函数均要做的逻辑抽象为一个装饰器,以此避免代码的重复。例如django中的@login_required函数用来在调用view之前先验证当前请求的用户是否登录。

装饰器的缺点?


装饰器也有缺点,例如上例中的wrapped.__name__返回的并不是wrapped这个名字而是wrapper。这是因为wrapped本身被替换成了一个wrapper闭包,所以返回的是这个闭包的各种属性,可以通过wrapped.func_closure等func_xx函数查看。这叫做装饰器的副作用(side affect)。因此如果原来调用wrapped的逻辑,需要用到这些属性(包括__doc____name__,__module__等),那么这些逻辑就会出错。

如何解决装饰器的副作用?

明白了装饰器的副作用的原因,我们就可以手动解决一下。将闭包的属性赋值为被装饰的函数的属性即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__module__ = func.__module__
return _wraps
@decorator
def wrapped(var):
print var
wrapped.__name__
wrapped.__doc__
wrapped.__module__
>>>'wrapped'
>>>''
>>>'__main__'

在装饰器中直接将装饰器的属性改为被装饰的对象的属性即可。

仔细看可以见到这种技巧的本质,是在装饰器中的wrapper函数之后干了点事情,当然可以写一个通用的装饰wrapper的装饰器来代替手工赋值。最后实现的类似这样:

1
2
3
4
5
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

利用一个装饰器来完成wrapper.__name__ = func.__name__的工作。因为需要func的属性值,因此这个装饰器需要传入func参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def wraps(wrapped):
def inner(func, wrapped=wrapped):
def _inner(*args, **kwargs):
return func(*args, **kwargs)
_inner.__name__ == wrapped.__name__
return inner
def decorator(func):
@wraps(func) #包装wrapper替换其属性
def wrapper(*args, **kwargs):
args = (s.title() for s in args)
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped(var):
print var
wrapped.__name__
>>>'wrapped'

可以看见最终wraps函数实现了functools.wraps的功能。

当然wraps有两点不够好:第一点是两层的_innerinner看起来比较难于理解;第二点,如果被装饰对象是一个类,那么类的属性将丢失。对于第一点,通过利用functools.partial将多层嵌套扁平化。对于第二点比较好解决,将类的__dict__属性同样赋值给闭包即可,因此增加updated参数实现要将哪些属性更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WRAPPER_ASSIGNMENT = ('__module', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__')
def update_wrapper(wrapper, wrapped,
assigned=WRAPPER_ASSIGNMENT,
updated=WRAPPER_UPDATES):
for attr in assigned:
setattr(wrapper, getattr(wrapped, attr))
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
return wrapper
def wraps(wrapped,
assigned=WRAPPER_ASSIGNMENT,
updated=WRAPPER_UPDATES):
return functools.partial(update_wrapper,
wrapped=wrapped,
assigned=assigned,
updated=updated)

这个wraps就是函数functools.wraps的实现,与传统的写法的不同点是将内部的update_wrapper写在了外面。wraps函数返回了一个这样的函数update_wrapper,这个函数的一些参数都已经给定,只剩下wrapper等待传入。

另外需要说明的是,partial函数是由C语言低层实现,不会修改update_wrapper的各种属性。