装饰器(decorator)的本质:函数闭包(function closure)的语法糖(Syntactic sugar)

1. 什么是函数闭包(function closure)

  • 函数式语言(函数是一等公民,可作为变量使用)中的术语
  • 函数闭包:一个函数,其参数和返回值都是函数
    • 用于增强函数功能
    • 面向切面编程(AOP)

先看一个例子:

  • 函数逻辑(查找奇数)和辅助功能(记录时间)耦合在一起了
  • 缺点:不方便修改,容易引起bug
  • 能不能将辅助功能从主要功能函数中抽离出来? ```python import time

def print_odds(): “”” 输出0-100之间所有奇数,并统计函数执行时间 “”” start_time = time.time() # 起始时间

  1. # 查找并输出所有奇数
  2. for i in range(100):
  3. if i % 2 ==1:
  4. print(i)
  5. end_time = time.time() # 结束时间
  6. print("it takes {} s to find all the odds".format(end_time-start_time))

if name == “main“: print_odds()

  1. 对上个例子进行修改:
  2. - 将辅助功能(记录时间)抽离成一个辅助函数,在辅助函数中调用主要功能函数
  3. - 优点:解耦,函数职责分离
  4. - 缺点:要通过辅助函数来调用主要功能函数,不方便
  5. - **我们的目标:能不能在调用主要功能函数时自动完成对时间的统计?**
  6. ```python
  7. import time
  8. def count_time(func):
  9. """
  10. 统计某个函数的运行时间
  11. """
  12. start_time = time.time() # 起始时间
  13. func() # 执行函数
  14. end_time = time.time() # 结束时间
  15. print("it takes {} s to find all the odds".format(end_time-start_time))
  16. def print_odds():
  17. """
  18. 输出0-100之间所有奇数,并统计函数执行时间
  19. """
  20. # 查找并输出所有奇数
  21. for i in range(100):
  22. if i % 2 ==1:
  23. print(i)
  24. if __name__ == "__main__":
  25. # print_odds()
  26. count_time(print_odds)

对上述修改再修改:使用闭包增强

  • 通过闭包增强主要功能函数print_odds,给它增加一个统计时间功能
  • 缺点:需要显式进行闭包增强 ```python import time

def print_odds(): “”” 输出0-100之间所有奇数,并统计函数执行时间 “””

  1. # 查找并输出所有奇数
  2. for i in range(100):
  3. if i % 2 ==1:
  4. print(i)

def count_time_wrapper(func): “”” 闭包,用于增强函数func,给函数func增加统计时间的功能 “”” def improved_func(): start_time = time.time() # 起始时间 func() # 执行函数 end_time = time.time() # 结束时间 print(“it takes {} s to find all the odds”.format(end_time-start_time))

return improved_func

if name == “main“:

  1. # 调用count_time_wrapper增强函数
  2. print_odds = count_time_wrapper(print_odds)
  3. print_odds()
  4. print_odds() # 调用的还是第一次增强的print_odds
  1. 对上述修改简化调用
  2. ```python
  3. import time
  4. def count_time_wrapper(func):
  5. """
  6. 闭包,用于增强函数func,给函数func增加统计时间的功能
  7. """
  8. def improved_func():
  9. start_time = time.time() # 起始时间
  10. func() # 执行函数
  11. end_time = time.time() # 结束时间
  12. print("it takes {} s to find all the odds".format(end_time-start_time))
  13. return improved_func
  14. @count_time_wrapper
  15. def print_odds():
  16. """
  17. 输出0-100之间所有奇数,并统计函数执行时间
  18. """
  19. # 查找并输出所有奇数
  20. for i in range(100):
  21. if i % 2 ==1:
  22. print(i)
  23. if __name__ == "__main__":
  24. # 调用count_time_wrapper增强函数
  25. # print_odds = count_time_wrapper(print_odds)
  26. print_odds()

2. 什么是语法糖(Syntactic sugar)

  • 指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用
    • 语法糖没有增加新功能,只是一种更方便的写法
    • 语法糖可以完全等价地转换为原本非语法糖的代码
  • 装饰器在第一次调用被装饰函数时进行增强
    • 下面两种方法完全等价
    • print_odds = count_time_wrapper(print_odds) print_odds()
    • @count_time_wrapper print_odds()
  • 装饰器@闭包函数名
  • 装饰器在第一次调用被装饰函数时进行增强
    • 增强时机?在第一次调用之前
    • 增强次数?只增强一次,参考第二次修改执行代码

3. 主要功能函数有参数的装饰器

上述例子的主要功能函数print_odds没有参数
现在考虑有参数的装饰器

  1. import time
  2. def count_time_wrapper(func):
  3. """
  4. 闭包,用于增强函数func,给函数func增加统计时间的功能
  5. """
  6. def improved_func():
  7. start_time = time.time() # 起始时间
  8. func() # 执行函数
  9. end_time = time.time() # 结束时间
  10. print("it takes {} s to find all the odds".format(end_time-start_time))
  11. return improved_func
  12. def count_odds(lim=100):
  13. cnt = 0
  14. for i in range(lim):
  15. if i % 2 == 1:
  16. cnt +=1
  17. return cnt
  18. if __name__ == "__main__":
  19. print("增强前")
  20. print(count_odds(lim=10000)) # 装饰前函数能正常返回,能接受参数
  21. print("-"*20)
  22. print("增强后")
  23. count_odds = count_time_wrapper(count_odds)
  24. print(count_odds())
  25. # 返回:None
  26. # 问题1:对于含有返回值的函数,调用闭包增强后,不能成功返回,但成功增强了辅助功能
  27. print(count_odds(lim=10000)) # 装饰后函数不能正常返回,不能接收参数
  28. # 直接报错
  29. # 问题2:对于含有参数的函数,调用闭包增强后,不能成功接收参数

为了解决上述两个问题,对装饰器函数进行修改:

  1. import time
  2. def count_time_wrapper(func):
  3. """
  4. 闭包,用于增强函数func,给函数func增加统计时间的功能
  5. """
  6. def improved_func(*args, **kwargs): # 增强函数应该把接收到的所有参数传给原函数
  7. start_time = time.time() # 起始时间
  8. ret = func(*args, **kwargs) # 执行函数
  9. # 解决问题2:不能接收参数
  10. end_time = time.time() # 结束时间
  11. print("it takes {} s to find all the odds".format(end_time-start_time))
  12. return ret # 解决问题1:不能正常返回值
  13. return improved_func
  14. def count_odds(lim=100):
  15. cnt = 0
  16. for i in range(lim):
  17. if i % 2 == 1:
  18. cnt +=1
  19. return cnt
  20. if __name__ == "__main__":
  21. print("增强前")
  22. print(count_odds(lim=10000)) # 装饰前函数能正常返回,能接受参数
  23. print("-"*20)
  24. print("增强后")
  25. count_odds = count_time_wrapper(count_odds)
  26. print(count_odds())
  27. # 返回:None
  28. # 问题1:对于含有返回值的函数,调用闭包增强后,不能成功返回,但成功增强了辅助功能
  29. print(count_odds(lim=10000)) # 装饰后函数不能正常返回,不能接收参数
  30. # 直接报错
  31. # 问题2:对于含有参数的函数,调用闭包增强后,不能成功接收参数

3. 面试题:多个装饰器的执行顺序

  1. import time
  2. def count_time_wrapper(func):
  3. """
  4. 闭包,用于增强函数func,给函数func增加统计时间的功能
  5. """
  6. def improved_func(*args, **kwargs): # 增强函数应该把接收到的所有参数传给原函数
  7. start_time = time.time() # 起始时间
  8. ret = func(*args, **kwargs) # 执行函数
  9. # 解决问题2:不能接收参数
  10. end_time = time.time() # 结束时间
  11. print("it takes {} s to find all the odds".format(end_time-start_time))
  12. return ret # 解决问题1:不能正常返回值
  13. return improved_func
  14. def log_wrapper(func):
  15. """
  16. 闭包,用于增强函数func:给func增加日志功能
  17. """
  18. def improved_func(*args, **kwargs):
  19. start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) # 起始时间
  20. ret = func(*args, **kwargs) # 执行函数
  21. end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) # 结束时间
  22. print("Logging: func:{} runs from {} to {}".format(func.__name__, start_time, end_time))
  23. return improved_func
  24. @count_time_wrapper
  25. @log_wrapper
  26. def count_odds(lim=100):
  27. cnt = 0
  28. for i in range(lim):
  29. if i % 2 == 1:
  30. cnt +=1
  31. return cnt
  32. if __name__ == "__main__":
  33. count_odds(lim=10000)

image.png


参考自: https://zhuanlan.zhihu.com/p/26724125 廖雪峰关于装饰器的讲解 【可能是b站上最好的Python装饰器教程-哔哩哔哩】 https://b23.tv/PWFur9B 【Python小技巧:装饰器(Decorator)-哔哩哔哩】 https://b23.tv/9cxdjXC