Python装饰器的执行顺序

================

引子

之所以写这篇博客,是因为今天在写代码时遇到了装饰器顺序带来的问题。我用Flask写web后端,需要对一个view方法进行多次装饰,其中一个装饰器用到了另一个装饰器产生的上下文,这就需要那个被依赖的装饰器先执行。所以在编写有相互依赖的装饰器时,我们的确需要多加小心。

一些基础

1. 它的原理

装饰器是Python的语法糖,它可以帮助你写成简洁、易懂、优雅的代码。在Python中一切皆对象,我们来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
def foo():
print 'hello world'
foo()
# output: hello world
fn = foo
type(fn)
# output: function
fn()
# output: hello world

这里我们定义了函数foo,我们甚至可以将foo赋值给fn,然后调用fn()会和直接调用foo()产生同样的效果。

闲话不说,我们继续看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decorator(func):
def wrapper():
print 'Do something before func.'
return func()
return wrapper
@decorator
def foo():
print 'hello world'
foo()
# output:
Do something before func.
hello world

我们看到,装饰器能够很容易地在调用被装饰函数之前做一些“其他有用的事情”。其实质就是公用代码的抽离,比如web后端的鉴权行为等。这样做的好处大概就是:

  1. 提高公用代码的利用率
  2. 优雅、易读、好维护
  3. 防止无关代码侵入被装饰函数

我们使用装饰器时,实际上发生了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
def decorator(func):
def wrapper():
print 'Do something before func.'
return func()
return wrapper
def foo():
print 'hello world'
decorator(foo)()
# output:
Do something before func.
hello world

装饰器的写法只是一个语法糖,上面的代码才是正真的行为,其实就是把待装饰的函数作为参数传入装饰器,做一些其他的事再返回这个函数。

2. 一些改进

当我们单纯的用上面的方式写装饰器时,代码的行为可能并不能使我们满意,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorator(func):
def wrapper(*args, **kwds):
"""this is wrapper"""
return func(*args, **kwds)
return wrapper
@decorator
def foo():
"""this is foo"""
pass
print foo.__name__
# output: wrapper
print foo.__doc__
# output: this is wrapper

我们的函数看起来明明是foo,但实际上已经被偷梁换柱了!这样的信息丢失不利于调试,我们自己看起来也很不爽。

幸运的是,强大的Python提供了一个很有用的工具方法functools.wraps,这个方法能使包装函数wrapper看起来更像是被包装的函数。注意我的用词是“”,说明通过wraps装饰,我们只是将func的一些元信息(metadata)赋给了wrapper,仅此而已!我们再对上述代码进行改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwds):
"""this is wrapper"""
return func(*args, **kwds)
return wrapper
@decorator
def foo():
"""this is foo"""
pass
print foo.__name__
# output: foo
print foo.__doc__
# output: this is foo

所以,这样使用装饰器才是正确的打开方式。

多装饰器的执行顺序

回到我们最初关心的问题—多装饰器的执行顺序。还是先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def first(func):
def wrapper(*args, **kwargs):
print 'first decorator called.'
return func(*args, **kwargs)
return wrapper
def second(func):
def wrapper(*args, **kwargs):
print 'second decorator called.'
return func(*args, **kwargs)
return wrapper
@first
@second
def foo():
print 'foo called.'
def bar():
print 'bar called.'
if __name__ == '__main__':
foo()
print '======================='
first(second(bar))()
# 运行结果
first decorator called.
second decorator called.
foo called.
=======================
first decorator called.
second decorator called.
bar called.

可以看出,当多个装饰器装饰同一个函数时,从下往上,每个装饰器都只负责装饰它下面的函数。听起来可能会很绕,我们看例子,foo同时被fisrtsecond装饰,则实际的行为是first(second(foo))()first装饰的函数是second装饰foo后得到的结果。

前面我们一直强调装饰器的作用就是做一些“其他有用的事情”,当有多个装饰器时,每一个都要做它自己“有用的事情”。在实际的开发中,这些“有用的事情”可能是相互依赖的,这时候如果我们搞不清楚放置装饰器的顺序,就会出现麻烦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
IS_LOGIN = False # 判断登录状态
def login_change(func):
@wraps(func)
def wrapper(*args, **kwargs):
global IS_LOGIN
IS_LOGIN = True
return func(*args, **kwargs)
return wrapper
def login_check(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not IS_LOGIN:
return unauth()
return func(*args, **kwargs)
return wrapper
@login_change
@login_check
def view1():
print 'view1 called'
@login_check
@login_change
def view2():
print 'view2 called'
def unauth():
print 'unauth called'
if __name__ == '__main__':
view1()
IS_LOGIN = False # reset login state
print '======================='
view2()
# 执行结果
view1 called
=======================
unauth called

上面我简单模拟了web后端处理登录认证的流程,全局变量IS_LOGIN代表登录状态,装饰器login_change做的“事情”是将IS_LOGIN置为True,装饰器login_check做的“事情”是检查登录状态,如果未登录转到unauth,否则放行到相应的view。显然这两个装饰器做的“事情”是相互影响的,它们的顺序也就变得至关重要了,view1view2不同的执行结果就是最有力的证明。

总结

装饰器是Python最实用的语法糖之一。使用装饰器能使我们的代码变得优雅、易读,结构清晰。但如果我们想正确使用它,就必须理解它的本质,尤其在多装饰器环境下,有些上下文的处理不当会带来意想不到的后果。所以当你在写这类程序时,一定要把顺序放在心上!