1. 引言
前面的文章中,讲到了 Python 装饰器的基础使用方式,在实际使用中,可能会遇到一个函数使用多个装饰器的情况,这个时候装饰器的顺序同样至关重要。本文将讨论装饰器的顺序如何影响函数的行为,并通过几个例子来说明。
2. 装饰器的顺序
当在一个函数上应用多个装饰器时,装饰器的执行顺序会影响最终的结果。
提到装饰器的顺序,很多人可能会说,装饰器是从内到外应用的,也就是说最靠近函数定义的装饰器会最先执行,而最外层的装饰器会最后执行。真的是这样吗,来看下面的例子:
def decorator_a(func):
print('decorator_a')
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
def decorator_b(func):
print('decorator_b')
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@decorator_a
@decorator_b
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
复制代码
执行结果:
decorator_b
decorator_a
Hello, Alice!
复制代码
看起来好像真的是先执行了装饰器 b,后执行了装饰器 a。但是事实真的如此吗,来给装饰器实际执行逻辑打个日志。
看下面的例子:
def decorator_a(func):
print('decorator_a')
def wrapper(*args, **kwargs):
print("Decorator A: Before function call")
result = func(*args, **kwargs)
print("Decorator A: After function call")
return result
return wrapper
def decorator_b(func):
print('decorator b')
def wrapper(*args, **kwargs):
print("Decorator B: Before function call")
result = func(*args, **kwargs)
print("Decorator B: After function call")
return result
return wrapper
@decorator_a
@decorator_b
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
复制代码
执行结果为:
decorator b
decorator_a
Decorator A: Before function call
Decorator B: Before function call
Hello, Alice!
Decorator B: After function call
Decorator A: After function call
复制代码
这个时候的结果就比较有趣了。来分析一下日志,装饰器 a 和 b 两个装饰器的确是按照之前提到的逻辑执行了,但是装饰器实际的逻辑却和我们预想的不一样。
为什么会出现这样的结果呢?为了理解这个问题,先来说一下装饰器的原理。
Python 装饰器的实现依赖于函数的可调用性和闭包的概念。
函数可以作为参数传递:在 Python 中,函数是一等公民,可以像变量一样被传递和赋值。这使得我们可以将函数作为参数传递给装饰器函数。
闭包:装饰器函数内部定义了一个新的函数(通常称为包装函数),这个包装函数可以访问装饰器函数的参数以及外部函数的局部变量。当装饰器函数返回包装函数时,包装函数就携带了这些信息,形成了一个闭包。
来看一个小例子
def decorator_test(func):
print('decorator_a')
def wrapper(*args, **kwargs):
print("Decorator A: Before function call")
result = func(*args, **kwargs)
print("Decorator A: After function call")
return result
return wrapper
# @decorator_test
def say_hello(name):
print(f"Hello, {name}!")
say_hello = decorator_test(say_hello)
say_hello("Alice")
复制代码
在上面的例子中,我注释了 @后面的装饰器,增加了一个赋值语句,其实,我们在用 @的时候就相当于执行了这个赋值语句,只不过用 @会简化我们的操作,同时更加直观易于理解。所以到这里我们应该能明白了,装饰器说白了就是将当前的函数当做参数传递给装饰器函数,然后装饰器函数执行后返回一个新的函数,这个函数替代了我们之前的函数,然后在真正调用函数的时候就会发现装饰器函数被执行了。
理解了上述原理以后,就会有一个问题了,既然函数会被当做参数传递,那么如果有两个装饰器的话,应该先传递谁呢,因为先传递的一定是原来的函数,后传递的已经是被第一个装饰器装饰过的函数了。
来看一个例子。
def decorator_test(func):
print('decorator_a')
def wrapper(*args, **kwargs):
print("Decorator A: Before function call")
result = func(*args, **kwargs)
print("Decorator A: After function call")
return result
return wrapper
def decorator_test2(func):
print('decorator_b')
def wrapper(*args, **kwargs):
print("Decorator B: Before function call")
result = func(*args, **kwargs)
print("Decorator B: After function call")
return result
return wrapper
# @decorator_test
def say_hello(name):
print(f"Hello, {name}!")
say_hello = decorator_test(decorator_test2(say_hello))
say_hello("Alice")
print('-' * 30)
@decorator_test
@decorator_test2
def say_hello2(name):
print(f"Hello, {name}!")
say_hello2("Alice2")
复制代码
运行结果:
decorator_b
decorator_a
Decorator A: Before function call
Decorator B: Before function call
Hello, Alice!
Decorator B: After function call
Decorator A: After function call
------------------------------
decorator_b
decorator_a
Decorator A: Before function call
Decorator B: Before function call
Hello, Alice2!
Decorator B: After function call
Decorator A: After function call
复制代码
从上面的例子可以看出,两种方式的运行结果是一致的,也就是说,say_hello = decorator_test(decorator_test2(say_hello))
等价于 say_hello2
函数的装饰器写法。
从上面两个图可以看到,装饰器函数本身是按照最靠近函数的优先执行的顺序执行的,但是 wrapper
函数是一层一层从最靠近函数的顺序嵌套执行的,也就是说,最外层的函数最先被执行,执行之后执行第二个装饰器,依次往最内层执行,然后依次返回,可以参考上图的数字序号。
4. 结论
综上所述,装饰器的顺序不能一概而论说内层装饰器先执行,准确的说应该是从外到内一层一层依次执行的,外层装饰器先执行,但是最后执行完,内层装饰器后执行,但是先执行完毕。有点类似于八股文, 大家把上面的代码执行一遍,打个断点跟一遍调试基本上就能明白了。
评论