【测试开发】Python装饰器 - 多个装饰器的执行顺序

Date: 2021.03.14
Author: jwensh

python装饰器的应用

装饰器是Python用于封装函数或公共代码的实现方式,来对指定的类或函数进行一个装饰,类似java中的注解,在日常的开发过程中会经常用到,当我们使用多个装饰器装饰时,它的执行顺序是什么样的?那我们来看下。

关键结果直接给出:

  • 装饰顺序:即python解释器执行的顺序,由下至上(运行时)
  • 调用顺序:由上至下(调用时)

1. 调用顺序自上而下的? (实验看看)

大部分涉及多个装饰器装饰的函数,调用顺序都会认为它们是自上而下的,比如下面这个例子

  • 装饰器函数
def AA(func):
    print('AA 1')
    def func_a(*args, **kwargs):
        print('AA 2')
        return func(*args, **kwargs)
    return func_a

def BB(func):
    print('BB 1')
    def func_b(*args, **kwargs):
        print('BB 2')
        return func(*args, **kwargs)
    return func_b

上面代码先定义两个装饰器函数: AA, BB, 这两个函数实现的功能是,接收一个函数作为参数然后返回创建的另一个函数,在这个创建的函数里调用接收的函数。

  • 被装饰的函数
@BB
@AA
def f(x):
    print('F')
    return x * 10

print(f(1))

定义的函数 f采用上面定义的AA, BB 作为装饰函数。

然后,我们以1为参数调用被装饰后的函数 f , AA, BB 的顺序是什么呢 ?

如果不假思索根据自下而上的原则来判断地话,先执行 AA 再执行 BB , 那么会先输出 AA 1, AA 2 再输出 BB 1 , BB 2, 但真实的实际上运行的结果如下:

AA 1
BB 1
BB 2
AA 2
F
10

1.1 函数函数调用的区别

为什么是先执行 BB 2 再执行 AA 2 呢?

为了彻底看清上面的问题,得先分清两个概念: 函数和函数调用。上面的例子中 f 称之为函数, f(1) 称之为函数调用,后者是对前者传入参数进行求值的结果。

在Python中函数也是一个对象,所以 f 是指代一个函数对象,它的值是函数本身, f(1) 是对函数的调用,它的值是调用的结果,这里的定义下 f(1) 的值10。同样地,拿上面的 AA 函数来说,它返回的是个函数对象 func_a ,这个函数对象是它内部定义的。在 func_a 里调用了函数 func ,将 func 的调用结果作为值返回。

1.2 装饰器函数在被装饰函数定义好后立即执行

当装饰器装饰一个函数时,究竟发生了什么? 现在简化我们的例子,假设是下面这样的:

def AA(func):
    print('AA 1')
    def func_a(*args, **kwargs):
        print('AA 2')
        return func(*args, **kwargs)
    return func_a
 
@AA
def f(x):
    print('f')
    return x * 2

这单个装饰器怎么理解?

@AA
def f(x):
    print('f')
    return x * 10

相当于

def f(x):
    print('f')
    return x * 10

f = AA(f)

所以,当python解释器执行这段代码时

1. AA 已经调用了,它以函数 f 作为参数, 返回它内部生成的一个函数,
2. 所以此后 f 指代的是 AA 里面返回的 func_a 。
3. 所以当以后调用 f 时,实际上相当于调用 func_a , 传给 f 的参数会传给 func_a ,
4. 在调用 func_a 时会把接收到的参数传给 func_a 里的 func 即 f ,
5. 最后返回的是 f 调用的值,所以在最外面看起来就像直接再调用 f 一样。

有点绕,可以细细体会

1.3 疑问的解释

当理清上面两方面概念时,就可以清楚地看清最原始的例子中发生了什么。
当解释器执行下面这段代码时,实际上按照从下到上的顺序已经依次调用了 AABB ,这是会输出对应的 AA 1BB 1 。 这时候 f 已经相当于 BB 里的 func_b

但因为 f 并没有被调用,所以 func_b 并没有被调用,依次类推 func_b 内部的 func_a 也没有调用,所以 AA 2BB 2 也不会被打印。

@BB
@AA
def f(x):
    print('f')
    return x * 10
    
print(f(1))

然后最后一行当我们对 f 传入参数1进行调用时, func_b 被调用了,它会先打印 BB 2 ,然后在 func_b 内部调用了 func_a 所以会再打印 AA 2, 然后再 func_a 内部调用的原来的 f, 并且将结果作为最终的返回。

当我们在上面的例子最后一行 print(f(1) 的调用去掉,看下结果

AA 1
BB 1

由上面的实验,在实际应用的场景中,当我们采用上面的方式写了两个装饰方法, 比如先验证有没有登录@is_login, 再验证权限够不够时 @is_permision_allowed 时,那装饰顺序:

@is_login
@is_permision_allowed
def my_func()
  # Do something
  return

2. 总结

其实可以这么理解,上面的函数装饰下面的函数,执行实际是自下而上,只是说 AA 执行后返回的 func_a 函数被抛给了装饰器 BB ,这里打印出函数名(func.__name__)看看

AA 1: f
BB 1: func_a
开始执行之前
BB 2: func_a
AA 2: f
F
10
  • 相当于f 函数被从下往上包装,然后再从上往下拆包装,所以实际上装饰器内部的函数是由上往下执行的

  • 装饰的时候是自下而上包装,但是装饰时我们并没有执行 f() 函数,所以是先 print 的装饰器AA , 再执行的装饰器BB,在包装完之后,f 函数被调用了,这个时候 f 的其实最终是等价于func_b的,而 func_b 是等价于 func_a 的,而 func_a 才等价于原来的 f 函数.

  • func_b 内容(BB 2: func_a) 会被先打印 , 可能会迷惑为什么装饰器 AA 和 func_a 同样是 def 却不同时执行,因为 return 一个函数名相当于是函数名的赋值,而不是函数名的调用,没有同时被调用。

  • AA 1, BB 1 这两行是初始化装饰器, BB 2 , AA 2 这两行是 f = BB(AA(f)) 执行闭包,第一个闭包是 BB 走到 func_b 然后执行到 AA,在 AA 执行到 func_a 再到被装饰函数 f , 是由上到下

上一篇:windows下安装Redis


下一篇:运算符