编程范式——装饰器模式
编程范式的定义:范式,即模式、方法。因此,编程范式,就是软件工程中的一类典型风格。
装饰器:修饰本体之外的器物。例如,窗帘装饰了窗子,本体就是窗子,装饰器就是窗帘。
“装饰器”模式的:“动态地给一个对象添加一些额外的职责。”
Python 的 Decorator
让我们暂时忘了什么代码,从一个 实际的编程场景开始,先写出能够实现功能的朴素代码,逐步优化代码。
需求
需要统计函数的执行时间,以便进行性能分析。
代码 V1
1def func(i: int) -> str:
2 start_time = time.time()
3
4 print(f"This is func {i}") # 函数主要的逻辑
5
6 end_time = time.time()
7 consume_time = end_time - start_time
8 print("consume time: {}".format(consume_time))
9
10 return f"func{i}"
实现思路:计算函数主体逻辑执行前后的时间差,即func
的执行时间。
优点:实现上简单 缺点:
- 没有扩展性(难以被其他函数 复用/扩展)
- 结构差(控制和逻辑没有解耦。控制指的是时间的计算,如何控制代码去获取函数的执行时间,这部分一般是可以标准化的;逻辑指的是 func本身要完成的主要功能,属于被业务驱动的代码,通常是非标准化的。)
方法抽取是重构手段中重要的一个手段。
方法抽取建议遵循“抽取到底(Extract till you Drop)原则”。抽取到没有什么可抽取为止
- 该原则目标一:每个函数只做一件事情
- 该原则目标二:代码读起来像是一篇好文章
代码 V1.1
优化思路:代码功能 主体 和 控制逻辑 分离
- 提取出一个函数
time_consume
,time_consume
接收一个函数func
作为参数,额外的参数的被接收函数的形参,time_consume
作用是统计func
函数的计算时间,返回值还是func
的返回。
1def time_consume(func, *args, **kwargs):
2
3 start_time = time.time()
4 result = func(*args, **kwargs)
5 end_time = time.time()
6 consume_time = end_time - start_time
7 print("consume time: {}".format(consume_time))
8
9 return result
10
11def func(i: int) -> str:
12 print(f"This is func {i}") # 函数主要的逻辑
13
14 return f"func{i}"
15
16if __name__ == "__main__":
17 a = time_consume(func, 2) # 统计 func 计算时间
18 a = func(2) # 不统计时间
到底为止,这个需求和代码都很漂亮地实现了。
优点:
- 函数逻辑和控制逻辑分离 缺点:
- 调用方式不够简洁
代码 V2.0
优化思路:能不能使用a = func1(2)
这种方式调用,默认执行控制逻辑?
如果可以,调用func(i)
-> 等同于 wrapper(func)(i)
-> 形式上表现为 func(i)
。
1def time_consume(func):
2
3 def wrapper(*args, **kwargs):
4 start_time = time.time()
5
6 result = func(*args, **kwargs)
7
8 end_time = time.time()
9 consume_time = end_time - start_time
10 print("consume time: {}".format(consume_time))
11
12 return result
13
14 return wrapper
15
16def func(i: int) -> str:
17 print(f"This is func {i}") # 函数主要的逻辑
18
19 return f"func{i}"
20
21if __name__ == "__main__":
22 a = time_consume(func1)
23 a(i = 2)
24 # a = time_consume(func1)(2)
与代码 V1.1 相比,time_consume
的实现变了。
思考,下面代码和上面有什么区别?
1def time_consume(func):
2
3 start_time = time.time()
4
5 def wrapper(*args, **kwargs):
6 # start_time = time.time()
7 result = func(*args, **kwargs)
8 end_time = time.time()
9 consume_time = end_time - start_time
10 print("consume time: {}".format(consume_time))
11
12 return result
13
14 return wrapper
上述代码,能够方便控制 被装饰的函数 什么时候被执行。a
就是被装饰后的函数,a()
代表执行函数 a。
相比 V1.1的代码,有什么改进吗?
- 可以看到
func1
的函数参数可以和包装函数time_consume
参数分离。 - 可以灵活调用被装饰的函数(初始化 - call 分离)
到这里,可以请出 python 的语法糖 @decorator
。
1@decorator
2def func():
3 pass
4
5# 等同于
6decorator(func)
7注意,这里只是被装饰的函数,
8decorator(func)() 就是被装饰的函数被执行
什么是装饰器?当你实现了一个 decorator_func
,能够这样子调用 decorator_func(any_func)(any_func_arg)
,那么 decorator_func
就是装饰器。
代码 V3.0
1def time_consume(func):
2
3 def wrapper(*args, **kwargs):
4 start_time = time.time()
5 result = func(*args, **kwargs)
6 end_time = time.time()
7 consume_time = end_time - start_time
8 print("consume time: {}".format(consume_time))
9
10 return result
11
12 return wrapper
13
14@time_consume
15def func(i: int) -> str:
16 print(f"func {i}")
17
18 return f"func{i}"
19
20if __name__ == "__main__":
21 func1(2)
区别在于:调用方式不同,使用了 python语法糖。
到此为止,算是完美完成这个任务。(鉴于演示,就没有加太多参数类型注解和代码注释)
细心的朋友可能注意到,函数使用了装饰器,就变成另一个函数了,即被装饰后的函数,那么原始函数的元信息就看不到了。
1print(func1.__name__) # output: wrapper
如何解决呢?
1from functools import wraps
2
3def time_consume(func):
4
5 @wraps(func)
6 def wrapper(*args, **kwargs):
7 start_time = time.time()
8 result = func(*args, **kwargs)
9 end_time = time.time()
10 consume_time = end_time - start_time
11 print("consume time: {}".format(consume_time))
12
13 return result
14
15 return wrapper
16
17...
18
19print(func1.__name__) # output: func1
解决了!
基于刚刚学习的知识,看看functools
库中的@wraps
装饰器是如何来保留原始函数的元信息。
1def wraps(wrapped,
2 assigned = WRAPPER_ASSIGNMENTS,
3 updated = WRAPPER_UPDATES):
4 """Decorator factory to apply update_wrapper() to a wrapper function
5
6 Returns a decorator that invokes update_wrapper() with the decorated
7 function as the wrapper argument and the arguments to wraps() as the
8 remaining arguments. Default arguments are as for update_wrapper().
9 This is a convenience function to simplify applying partial() to
10 update_wrapper().
11 """
12 return partial(update_wrapper, wrapped=wrapped,
13 assigned=assigned, updated=updated)
14
15def update_wrapper(wrapper,
16 wrapped,
17 assigned = WRAPPER_ASSIGNMENTS,
18 updated = WRAPPER_UPDATES):
19 """Update a wrapper function to look like the wrapped function
20
21 wrapper is the function to be updated
22 wrapped is the original function
23 assigned is a tuple naming the attributes assigned directly
24 from the wrapped function to the wrapper function (defaults to
25 functools.WRAPPER_ASSIGNMENTS)
26 updated is a tuple naming the attributes of the wrapper that
27 are updated with the corresponding attribute from the wrapped
28 function (defaults to functools.WRAPPER_UPDATES)
29 """
30 for attr in assigned:
31 try:
32 value = getattr(wrapped, attr)
33 except AttributeError:
34 pass
35 else:
36 setattr(wrapper, attr, value)
37 for attr in updated:
38 getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
39 # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
40 # from the wrapped function when updating __dict__
41 wrapper.__wrapped__ = wrapped
42 # Return the wrapper so this can be used as a decorator via partial()
43 return wrapper
functools.wraps
的实现原理是通过 partial
对象和 update_wrapper
函数来实现的。
当使用 @functools.wraps(func)
装饰一个函数时,functools.wraps
会返回一个 partial
对象,这个对象在被调用时会执行 update_wrapper
函数,将原函数的相应属性拷贝给新的函数(即装饰器内部的函数)。
这样,即使函数被装饰,它的元信息也会被保留,使得代码更加清晰和易于理解。
Go语言的 Decorator
1package main
2
3import "fmt"
4
5func decorator(f func(s string)) func(s string) {
6 return func(s string) {
7 fmt.Println("Started")
8 f(s)
9 fmt.Println("Done")
10 }
11}
12
13func Hello(s string) {
14 fmt.Println(s)
15}
16
17func main() {
18 hello := decorator(Hello)
19 hello("Hello")
20}
跟 Python 的玩法几乎是一样的,只不过 Go 不支持 @decorator 语法糖,在调用上不如 Python 优雅。
Java语言的注解
Java 实现一个注解,该注解判断变量,如果<=1,则抛出异常。
1// 自定义注解
2import java.lang.annotation.ElementType;
3import java.lang.annotation.Retention;
4import java.lang.annotation.RetentionPolicy;
5import java.lang.annotation.Target;
6
7
8// 自定义异常
9public class ValueException extends RuntimeException {
10 public ValueException(String message) {
11 super(message);
12 }
13}
14
15
16// 1. 自定义注解
17@Target(ElementType.FIELD) // 指定注解可以用于字段
18@Retention(RetentionPolicy.RUNTIME) // 指定注解在运行时有效
19public @interface CheckValue {
20 String message() default "Value must be greater than 1";
21}
22
23
24// 2. 自定义注解处理器
25public class ValueChecker {
26 public static void checkValue(Object obj) {
27 Class<?> clazz = obj.getClass();
28 for (Field field : clazz.getDeclaredFields()) {
29 if (field.isAnnotationPresent(CheckValue.class)) {
30 try {
31 field.setAccessible(true); // 让被注解的字段可见,即使是 private
32 Object value = field.get(obj);
33 if (value instanceof Integer && (Integer) value <= 1) {
34 throw new ValueException(field.getAnnotation(CheckValue.class).message());
35 }
36 } catch (IllegalAccessException e) {
37 e.printStackTrace();
38 }
39 }
40 }
41 }
42}
43
44// 使用示例
45public class Test {
46 @CheckValue
47 private int value = 0;
48
49 public static void main(String[] args) {
50 Test test = new Test();
51 ValueChecker.checkValue(test); // 这里会抛出ValueException
52 }
53}
Java 通用反射(Reflection)来实现注解,其行为看起来和 Python的装饰器是一样的
Python类方式的 Decorator
1class myDecorator(object):
2 def __init__(self, fn=None):
3 print("inside myDecorator.__init__()")
4 self.fn = fn
5
6 def __call__(self, *args, **kwargs):
7 print("inside myDecorator.__call__()")
8 return self.fn(*args, **kwargs)
9
10
11@myDecorator
12def aFunction(i: int):
13 print(f"inside aFunction({i})")
14
15
16if __name__ == "__main__":
17 aFunction(1)
18 # myDecorator(aFunction)(1)
__call__
能够允许像调用函数一样调用类
aFunction()
翻译过来,就是 myDecorator(aFunction)
典型应用
1. 为函数加缓存
1from functools import wraps
2def memoization(fn):
3 cache = {}
4 miss = object()
5
6 @wraps(fn)
7 def wrapper(*args):
8 result = cache.get(args, miss)
9 if result is miss:
10 result = fn(*args)
11 cache[args] = result
12 return result
13
14 return wrapper
15
16@memoization
17def fib(n):
18 if n < 2:
19 return n
20 return fib(n - 1) + fib(n - 2)
2. 注册路由
1class MyApp():
2 def __init__(self):
3 self.func_map = {}
4
5 def register(self, name):
6 def func_wrapper(func):
7 self.func_map[name] = func
8 return func
9 return func_wrapper
10
11 def call_method(self, name=None):
12 func = self.func_map.get(name, None)
13 if func is None:
14 raise Exception("No function registered against - " + str(name))
15 return func()
16
17app = MyApp()
18
19@app.register('/')
20def main_page_func():
21 return "This is the main page."
22
23@app.register('/next_page')
24def next_page_func():
25 return "This is the next page."
26
27print app.call_method('/')
28print app.call_method('/next_page')
RAM Automation Practice
为函数加锁
1def synchronized(func):
2 """
3 只有一个线程可以同时执行该函数,确保了多个线程访问该函数不会互相干扰
4 func: 需要同步的函数
5 """
6 func.__lock__ = threading.Lock()
7
8 def lock_func(*args, **kwargs):
9 with func.__lock__:
10 return func(*args, **kwargs)
11
12 return lock_func
为测试用例加 allure 信息
1def allure_decorator(title=None):
2 def decorator(func):
3 @functools.wraps(func)
4 def wrapper(*args, **kwargs):
5 file_path = inspect.getfile(func)
6 line_number = inspect.getsourcelines(func)[1]
7 line_range = {"start_line": line_number, "end_line": line_number}
8 allure.dynamic.title(f"{func.__name__ if title is None else title}")
9 allure.attach(
10 get_code_authors(file_path, line_range),
11 "Last modified by",
12 allure.attachment_type.JSON,
13 )
14 return func(*args, **kwargs)
15
16 return wrapper
17
18 return decorator
总结
- 装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数,这个新函数通常会包含原函数的功能,并添加一些额外的行为。
- 装饰器能够提高代码的扩展能力,将控制代码和业务功能代码解耦