找回密码
 立即注册
首页 业界区 业界 Python 中多个装饰器执行顺序验证

Python 中多个装饰器执行顺序验证

接快背 5 小时前
关于 Python 装饰器执行时的顺序问题,一直以来都保持粗略的理解概念:

  • 装饰器相当于函数调用的语法糖,因此在函数执行时,会从最内层括号开始,逐层向外执行。从代码文本上看,就是距离被修饰函数越近的装饰器,越先执行
  • 原始的装饰器会覆盖被修饰函数的__name__等元数据,需要使用functools.wraps修饰wrapper函数,保留被修饰函数的元数据
  • Python 代码存在编译时运行时的区别,因此,装饰器中代码的执行顺序,不总是被装饰函数最先执行
最近遇到一些问题,对以上概念的理解有些模糊,尝试通过代码验证。执行 Python 版本为 3.9.12。
单模块执行

测试代码
  1. #!/usr/bin/python
  2. # encoding: utf-8
  3. def deco1(func):
  4.     print('Deco 1-1')
  5.     def wrapper(*args, **kwargs):
  6.         print('Deco 1 - wrapper 1')
  7.         r = func(*args, **kwargs)
  8.         print('Deco 1 - wrapper 2')
  9.         return r
  10.     print('Deco 1-2')
  11.     return wrapper
  12. def deco2(func):
  13.     print('Deco 2-1')
  14.     def wrapper(*args, **kwargs):
  15.         print('Deco 2 - wrapper 1')
  16.         r = func(*args, **kwargs)
  17.         print('Deco 2 - wrapper 2')
  18.         return r
  19.     print('Deco 2-2')
  20.     return wrapper
  21. def deco3(func):
  22.     print('Deco 3-1')
  23.     def wrapper(*args, **kwargs):
  24.         print('Deco 3 - wrapper 1')
  25.         r = func(*args, **kwargs)
  26.         print('Deco 3 - wrapper 2')
  27.         return r
  28.     print('Deco 3-2')
  29.     return wrapper
  30. @deco3
  31. @deco2
  32. @deco1
  33. def test():
  34.     print("Test func")
  35. if __name__ == '__main__':
  36.     print('Test begin')
  37.     test()
  38.     print('Test end')
复制代码
测试代码中,deco1、deco2 和 deco3 装饰器都是同步函数,只有输出日志是不同的,因此,可以认为输出日志的顺序,即是代码执行的顺序。
执行结果

代码执行结果如下:
  1. Deco 1-1
  2. Deco 1-2
  3. Deco 2-1
  4. Deco 2-2
  5. Deco 3-1
  6. Deco 3-2
  7. Test begin
  8. Deco 3 - wrapper 1
  9. Deco 2 - wrapper 1
  10. Deco 1 - wrapper 1
  11. Test func
  12. Deco 1 - wrapper 2
  13. Deco 2 - wrapper 2
  14. Deco 3 - wrapper 2
  15. Test end
复制代码
执行结果分析

分析执行结果的顺序可以发现:

  • 进入模块__main__入口之前,会先“编译”装饰器代码,因此 "Test begin" 之前会打印Deco 1-1 等日志
  • 在“Test begin”执行后,Deco 3 - wrapper 1 -> Deco 2 - wrapper 1 -> Deco 1 - wrapper 1 依次出现,与装饰器从上到下的应用顺序相符,而“Test func”之后,Deco 1 - wrapper 2 -> Deco 2 - wrapper 2 -> Deco 3 - wrapper 2 依次执行,顺序变成后进先出
  • 所有装饰器代码执行结束,才轮到最后的“Test end”
多个装饰器的“编译”顺序,符合“距离最近的先执行”的印象

如果调整装饰器的应用顺序,让 deco2 最先修饰 test,关键代码如下:
  1. @deco3
  2. @deco1
  3. @deco2
  4. def test():
  5.     print("Test func")
复制代码
执行时输出结果如下,可以发现“Test begin”之前的部分中,deco2最先被调用
  1. Deco 2-1
  2. Deco 2-2
  3. Deco 1-1
  4. Deco 1-2
  5. Deco 3-1
  6. Deco 3-2
  7. Test begin
  8. Deco 3 - wrapper 1
  9. Deco 1 - wrapper 1
  10. Deco 2 - wrapper 1
  11. Test func
  12. Deco 2 - wrapper 2
  13. Deco 1 - wrapper 2
  14. Deco 3 - wrapper 2
  15. Test end
复制代码
结论 v1.0

分析上面代码的执行顺序,可以得到以下结论:

  • 执行被装饰的函数之前,存在类似“编译”的阶段,会执行装饰器中 wrapper 函数之外的代码
  • 多个装饰器修饰同一个函数的情况下,最外层(即写在函数代码块最上面一行)的装饰器的 wrapper 函数中,被装饰函数之前的代码最先被执行,然后是次外层的装饰器中被装饰函数之前的代码,依此类推,直到真正执行被装饰函数;如果 wrapper 函数内存在执行后的代码,则执行顺序与 wrapper 函数内被装饰函数执行前代码的执行顺序相反,可以认为全部代码的执行顺序符合deco3(deco2(deco1(test())))的函数式执行顺序,与文档说明相符
多模块执行

测试装饰器是否应用编译缓存。将装饰器代码与测试代码放在不同的模块中。
测试代码

decos.py
  1. #!/usr/bin/python
  2. # encoding: utf-8
  3. def deco1(func):
  4.     print('Deco 1-1')
  5.     def wrapper(*args, **kwargs):
  6.         print('Deco 1 - wrapper 1')
  7.         r = func(*args, **kwargs)
  8.         print('Deco 1 - wrapper 2')
  9.         return r
  10.     print('Deco 1-2')
  11.     return wrapper
  12. def deco2(func):
  13.     print('Deco 2-1')
  14.     def wrapper(*args, **kwargs):
  15.         print('Deco 2 - wrapper 1')
  16.         r = func(*args, **kwargs)
  17.         print('Deco 2 - wrapper 2')
  18.         return r
  19.     print('Deco 2-2')
  20.     return wrapper
  21. def deco3(func):
  22.     print('Deco 3-1')
  23.     def wrapper(*args, **kwargs):
  24.         print('Deco 3 - wrapper 1')
  25.         r = func(*args, **kwargs)
  26.         print('Deco 3 - wrapper 2')
  27.         return r
  28.     print('Deco 3-2')
  29.     return wrapper
复制代码
decorator-test.py
  1. #!/usr/bin/python
  2. # encoding: utf-8
  3. from decos import deco1, deco2, deco3
  4. @deco2
  5. def test2():
  6.     print("Test 2 func")
  7. @deco3
  8. @deco2
  9. @deco1
  10. def test():
  11.     print("Test func")
  12. if __name__ == '__main__':
  13.     print('Test begin')
  14.     test2()  # 连续执行多个被装饰函数
  15.     print("==" * 10)
  16.     test()
  17.     print('Test end')
复制代码
执行结果
  1. Deco 2-1
  2. Deco 2-2
  3. Deco 1-1
  4. Deco 1-2
  5. Deco 2-1
  6. Deco 2-2
  7. Deco 3-1
  8. Deco 3-2
  9. Test begin
  10. Deco 2 - wrapper 1
  11. Test 2 func
  12. Deco 2 - wrapper 2
  13. ====================
  14. Deco 3 - wrapper 1
  15. Deco 2 - wrapper 1
  16. Deco 1 - wrapper 1
  17. Test func
  18. Deco 1 - wrapper 2
  19. Deco 2 - wrapper 2
  20. Deco 3 - wrapper 2
  21. Test end
复制代码
结果分析

分析以上执行结果,可以发现:

  • Deco 2-1出现了两次,可以认为多个被装饰函数一起被执行时,装饰器 wrapper 函数之外的代码会被重复执行,执行顺序符合代码中被装饰函数的声明顺序
  • wrapper 函数内,被装饰函数前后的代码,它们的执行顺序与单模块的情况下相同
结论

Python 中的装饰器相当于语法糖,实际执行时,代码执行顺序与多层函数包裹目标函数的写法的执行顺序一致(即 deco3(deco2(deco1(test()))) )。
在简单装饰器的场景下,例如装饰器的 wrapper 函数中直接return func(**args, **kwargs)的写法,装饰器函数本身、内部 wrapper 函数中不包含额外逻辑的情况下,可以认为被装饰函数先执行并返回结果,然后越靠近被装饰函数的装饰器越先返回。
如果装饰器本身包含较复杂的逻辑,例如测试代码中 wrapper 函数执行func前后都存在其他逻辑的写法,则需要参考多层函数的执行顺序,wrapper 函数之外的代码会先于被执行代码执行,wrapper 函数中func前的代码会按从最外层装饰器到最内层的顺序执行,func后的代码则反过来,最内层装饰器的先执行,符合多层函数调用堆栈的抽象。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册