================
引子
之所以写这篇博客,是因为今天在写代码时遇到了装饰器顺序带来的问题。我用Flask
写web后端,需要对一个view方法进行多次装饰,其中一个装饰器用到了另一个装饰器产生的上下文,这就需要那个被依赖的装饰器先执行。所以在编写有相互依赖的装饰器时,我们的确需要多加小心。
一些基础
1. 它的原理
装饰器是Python的语法糖,它可以帮助你写成简洁、易懂、优雅的代码。在Python中一切皆对象,我们来看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11
| def foo(): print 'hello world' foo() fn = foo type(fn) fn()
|
这里我们定义了函数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() Do something before func. hello world
|
我们看到,装饰器能够很容易地在调用被装饰函数之前做一些“其他有用的事情”。其实质就是公用代码的抽离,比如web后端的鉴权行为等。这样做的好处大概就是:
- 提高公用代码的利用率
- 优雅、易读、好维护
- 防止无关代码侵入被装饰函数
我们使用装饰器时,实际上发生了什么呢?
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)() 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__ print foo.__doc__
|
我们的函数看起来明明是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__ print foo.__doc__
|
所以,这样使用装饰器才是正确的打开方式。
多装饰器的执行顺序
回到我们最初关心的问题—多装饰器的执行顺序。还是先看一段代码:
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
同时被fisrt
和second
装饰,则实际的行为是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 print '=======================' view2() view1 called ======================= unauth called
|
上面我简单模拟了web后端处理登录认证的流程,全局变量IS_LOGIN
代表登录状态,装饰器login_change
做的“事情”是将IS_LOGIN
置为True
,装饰器login_check
做的“事情”是检查登录状态,如果未登录转到unauth
,否则放行到相应的view
。显然这两个装饰器做的“事情”是相互影响的,它们的顺序也就变得至关重要了,view1
和view2
不同的执行结果就是最有力的证明。
总结
装饰器是Python最实用的语法糖之一。使用装饰器能使我们的代码变得优雅、易读,结构清晰。但如果我们想正确使用它,就必须理解它的本质,尤其在多装饰器环境下,有些上下文的处理不当会带来意想不到的后果。所以当你在写这类程序时,一定要把顺序放在心上!