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 装饰器的实现依赖于函数的可调用性和闭包的概念。

  1. 函数可以作为参数传递:在 Python 中,函数是一等公民,可以像变量一样被传递和赋值。这使得我们可以将函数作为参数传递给装饰器函数。
  2. 闭包:装饰器函数内部定义了一个新的函数(通常称为包装函数),这个包装函数可以访问装饰器函数的参数以及外部函数的局部变量。当装饰器函数返回包装函数时,包装函数就携带了这些信息,形成了一个闭包。

来看一个小例子

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 函数的装饰器写法。

image-20241104165804329

image-20241104172532302

从上面两个图可以看到,装饰器函数本身是按照最靠近函数的优先执行的顺序执行的,但是 wrapper 函数是一层一层从最靠近函数的顺序嵌套执行的,也就是说,最外层的函数最先被执行,执行之后执行第二个装饰器,依次往最内层执行,然后依次返回,可以参考上图的数字序号。

4. 结论

综上所述,装饰器的顺序不能一概而论说内层装饰器先执行,准确的说应该是从外到内一层一层依次执行的,外层装饰器先执行,但是最后执行完,内层装饰器后执行,但是先执行完毕。有点类似于八股文, 大家把上面的代码执行一遍,打个断点跟一遍调试基本上就能明白了。


LLLibra146
35 声望6 粉丝

会修电脑的程序员