0%

Python之Decorator装饰器

这里介绍Python中的Decorator装饰器

abstract.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函数进行装饰
test1_new = my_deco( test1 )
test1_new()

figure 1.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

自定义装饰器

基本实现

这里利用闭包来定义一个装饰器,实现对被装饰函数的功能增强。具体地,用于记录被装饰函数调用时的入参、出参

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):
"""
日志记录
"""
# 这里内层函数使用了外层函数的变量func,此时就产生了闭包
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}")

# 由于使用了装饰器,本质相当于 three_sum_1 = my_decorator_log_1(three_sum_1)
# 此时,three_sum_1的__name__、__doc__ 等属性会被修改为装饰后函数的
print("three_sum_1.__name__: ", three_sum_1.__name__)
print("three_sum_1.__doc__: ", three_sum_1.__doc__)

figure 3.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")

# 故,需要把被装饰函数的__name__等属性复制到装饰后的函数中
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

@functools.wraps装饰器

除了手动复制解决上述问题外,还可以对装饰器返回的函数应用 @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")

# 对装饰器返回的函数应用 @functools.wraps(被装饰函数) 装饰器即可,实现相关属性的复制
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

参数化装饰器

由于,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åalog 会报错
@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

常用内置装饰器

@functools.lru_cache 缓存装饰器

该装饰器使用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

@functools.singledispatch 单分派装饰器

由于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

而从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。其可以根据第一个参数的类型进行分派
@functools.singledispatch
def age_info_2(age):
print(f"Age Info: {age}")

# 对于,其他重载版本而言,其函数名不重要可直接使用_命名即可
# 同时,使用 @<基础函数的名称>.register(参数类型) 进行装饰
@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

参考文献

  1. Python编程·第3版:从入门到实践 Eric Matthes著
  2. Python基础教程·第3版 Magnus Lie Hetland著
  3. 流畅的Python·第1版 Luciano Ramalho著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝