编程范式——装饰器模式

编程范式的定义:范式,即模式、方法。因此,编程范式,就是软件工程中的一类典型风格。

装饰器:修饰本体之外的器物。例如,窗帘装饰了窗子,本体就是窗子,装饰器就是窗帘。

“装饰器”模式的:“动态地给一个对象添加一些额外的职责。”

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_consumetime_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

总结

  • 装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数,这个新函数通常会包含原函数的功能,并添加一些额外的行为。
  • 装饰器能够提高代码的扩展能力,将控制代码和业务功能代码解耦