这里介绍Python中的Decorator装饰器
![abstract.png](https://s2.loli.net/2024/07/21/bNwkTJ6WRFxC5aj.png)
基本实践
定义
装饰器是一个可调用的对象。其入参是另一个函数(即被装饰的函数)并返回一个新函数。装饰器会在被装饰的函数定义之后立即运行。通常发生在Python导入、加载模块时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| title = "装饰器定义" print(f"------------ {title} -------------------\n\n")
def hello(): print("Hello~~~~")
def my_deco(func): print(f"装饰器 my_deco 运行, func --->>> {func}") return hello
def test1(): print("This is a test 1 func ...")
@my_deco def test2(): print("This is a test 2 func ...")
print("\n------- 未使用装饰器的函数 -------------") test1()
print("\n------- 使用了装饰器的函数 -------------") test2()
print("\n------- 将函数作为参数,返回新函数 -------------")
test1_new = my_deco( test1 ) test1_new()
|
![figure 1.png](https://s2.loli.net/2024/07/21/r4a5WgNokDULeKp.png)
叠加多个装饰器
当对一个被装饰函数叠加多个装饰器时,其是有顺序的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| title = "使用多个装饰器" print(f"------------ {title} -------------------\n\n")
def my_d1(func): print(f"装饰器 my_d1 运行, func --->>> {func}") return lambda : print("this is my d1")
def my_d2(func): print(f"装饰器 my_d2 运行, func --->>> {func}") return lambda : print("this is my d2")
def my_d3(func): print(f"装饰器 my_d3 运行, func --->>> {func}") return lambda : print("this is my d3")
@my_d3 @my_d2 @my_d1 def eat_food(): print("eat food")
def drink_milk(): print("drink milk")
print("\n--------------------------") eat_food()
print("\n--------------------------") drink_milk_new = my_d3( my_d2( my_d1(drink_milk) ) ) drink_milk_new()
|
![figure 2.png](https://s2.loli.net/2024/07/21/uodChkbJIi8QjNB.png)
自定义装饰器
基本实现
这里利用闭包来定义一个装饰器,实现对被装饰函数的功能增强。具体地,用于记录被装饰函数调用时的入参、出参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| title = "自定义装饰器" print(f"------------ {title} -------------------\n")
def my_decorator_log_1(func): def logger_1(*args): """ 日志记录 """ print("[Start] ->>> input: ", args) res = func(*args) print("[End] ->>> output: ", res) return res return logger_1
@my_decorator_log_1 def three_sum_1(a,b,c): """ 计算三数之和 """ return a+b+c
res = three_sum_1(1,2,4) print(f"res ------->>>> {res}")
print("three_sum_1.__name__: ", three_sum_1.__name__) print("three_sum_1.__doc__: ", three_sum_1.__doc__)
|
![figure 3.png](https://s2.loli.net/2024/07/21/trPYIT8ZuenoHFj.png)
手动复制
上述装饰器 @my_decorator_log_1 的问题在于,其会丢失被装饰函数原本的信息。例如__name__、__doc__等属性。解决方法之一是我们在自定义装饰器时,进行手动复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| title = "自定义装饰器 #2" print(f"------------ {title} -------------------\n")
def my_decorator_log_2(func): def logger_2(*args): """ 日志记录 """ print("[Start] ->>> input: ", args) res = func(*args) print("[End] ->>> output: ", res) return res
logger_2.__name__ = func.__name__ logger_2.__doc__ = func.__doc__ return logger_2
@my_decorator_log_2 def three_sum_2(a,b,c): """ 计算三数之和 """ return a+b+c
res = three_sum_2(3,7,5) print(f"res --->>> {res}") print("three_sum_2.__name__: ", three_sum_2.__name__) print("three_sum_2.__doc__: ", three_sum_2.__doc__)
|
![figure 4.png](https://s2.loli.net/2024/07/21/8fLAwz6Ul7gaBhr.png)
除了手动复制解决上述问题外,还可以对装饰器返回的函数应用 @functools.wraps() 装饰器即可,实现相关属性的自动复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import functools
title = "自定义装饰器 #3" print(f"------------ {title} -------------------\n")
def my_decorator_log_3(func): @functools.wraps(func) def logger_3(*args): """ 日志记录 """ print("[Start] ->>> input: ", args) res = func(*args) print("[End] ->>> output: ", res) return res return logger_3
@my_decorator_log_3 def three_sum_3(a,b,c): """ 计算三数之和 """ return a+b+c
res = three_sum_3(3,7,5) print(f"res ------->>> {res}") print("three_sum_3.__name__: ", three_sum_3.__name__) print("three_sum_3.__doc__: ", three_sum_3.__doc__)
|
![figure 5.png](https://s2.loli.net/2024/07/21/mrgfqDWn9xXyVlG.png)
参数化装饰器
由于,Python会把被装饰函数作为第一个参数传给装饰器函数。故为了实现装饰器接受其他参数,需要在装饰器实现的外层再嵌套一层函数。例如,下面的my_decorator_log_4函数,我们在其外面再加一层函数my_log,其可以接受其他参数,然后在内部的嵌套函数中进行使用,并返回最终的my_decorator_log_4函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import functools
title = "参数化装饰器" print(f"------------ {title} -------------------\n")
def my_log(join=">>>"): def my_decorator_log_4(func): @functools.wraps(func) def logger_4(*args): """ 日志记录 """ print("[Start]: ", join, "input: ", args) res = func(*args) print("[End]: ", join, "output: ", res) return res return logger_4 return my_decorator_log_4
@my_log("====") def three_sum_4(a,b,c): """ 计算三数之和 """ return a+b+c
@my_log() def three_sum_5(a,b,c): """ 计算三数之和 """ return a+b+c
def three_sum_6(a,b,c): """ 计算三数之和 """ return a+b+c
print("\n------------------------------------") three_sum_4(1,2,4) print("three_sum_4.__name__: ", three_sum_4.__name__) print("three_sum_4.__doc__: ", three_sum_4.__doc__)
print("\n------------------------------------") three_sum_5(1,2,5) print("three_sum_5.__name__: ", three_sum_5.__name__) print("three_sum_5.__doc__: ", three_sum_5.__doc__)
print("\n------------------------------------") three_sum_6_new = my_log("||||")(three_sum_6) three_sum_6_new(10,20,40) print("three_sum_6_new.__name__: ", three_sum_6_new.__name__) print("three_sum_6_new.__doc__: ", three_sum_6_new.__doc__)
|
![figure 6.png](https://s2.loli.net/2024/07/21/7dhJ9BkLPvmNu5F.png)
常用内置装饰器
该装饰器使用LRU淘汰策略来保存函数的调用结果,可以有效避免递归函数的重复计算。该装饰器有个maxsize参数,用于保存函数调用结果的最大数量。超过数量后使用LRU策略进行淘汰。不指定则使用默认值128。Note: 由于该装饰器是一个参数装饰器,故使用时必须添加括号,即使未带参数。但从Python 3.8开始,为防止开发者忘记添加括号,其对进行优化。故此时直接使用 @functools.lru_cache,不添加括号也是可以的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import functools
title = "@functools.lru_cache 装饰器" print(f"------------ {title} -------------------\n")
@functools.lru_cache def my_fib_num_1(n): print(f"happen call func, param n : {n}") if n<2: return n return my_fib_num_1(n-1) + my_fib_num_1(n-2)
print("\n------------------ 计算fib(4) -----------------------------") num1 = my_fib_num_1(6) print(f"num2 : {num1}")
print("\n--------------------- 计算fib(7) ---------------") num2 =my_fib_num_1(12) print(f"num2 : {num2}")
|
![figure 7.png](https://s2.loli.net/2024/07/21/tjOVRq7dAJrXgyM.png)
由于Python不支持函数重载,故通常我们需要借助 类型判断和if-else的组合拳 来实现对不同入参的处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12
| def age_info_1(age): if type(age) == int: print(f"我的年龄是{age}岁") elif type(age) == str: print(f"My age is {age}") else: print(f"Age Info: {age}")
age_info_1(18) age_info_1("18") age_info_1([18])
|
![figure 8.png](https://s2.loli.net/2024/07/21/sTtec49a5jIJupD.png)
而从Python 3.4版本中引入了 @functools.singledispatch 单分派装饰器。在基础版本的函数上添加 装饰器 @functools.singledispatch。其可以根据第一个参数的类型进行分派。对于其他的具体重载版本而言,其函数名不重要,我们直接使用_命名即可。同时使用 @<基础函数的名称>.register(参数类型) 装饰器进行装饰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import functools
title = "@functools.singledispatch 装饰器" print(f"------------ {title} -------------------\n")
@functools.singledispatch def age_info_2(age): print(f"Age Info: {age}")
@age_info_2.register(int) def _(age): print(f"我的年龄是{age}岁")
@age_info_2.register(str) def _(age): print(f"My age is {age}")
age_info_2(17) age_info_2("17") age_info_2([17])
|
![figure 9.png](https://s2.loli.net/2024/07/21/krYauTlPXBqRfCJ.png)
参考文献
- Python编程·第3版:从入门到实践 Eric Matthes著
- Python基础教程·第3版 Magnus Lie Hetland著
- 流畅的Python·第1版 Luciano Ramalho著