Python 类实现的装饰器及简陋 REST API

学习了函数实现的 Python 装饰器后,关于装饰器的内容还没完。Python 装饰器还是属于元编程的范畴,一谈到元(Meta), 元编程,往往能用简单的方式实现比较神奇的效果 -- 小渣男的非死不可除外。Python 还允许用类来实现装饰器,原理上就是能让 Python 对象函数用,见之前的一篇 Python 对象当函数使用及动态添加方法。关键就是类实现 __call__ 函数,对象就变成 callable, 与函数的装饰器实现归纳起来就是:一个 Python 类型能不能用 @ 当作装饰器来用只需看它是否是 callable
1class Duck:
2    def __call__(self):
3        print('quack')
4        
5duck = Duck()
6print(callable(duck))  # True
7duck()

而且因为有了类,带属性的装饰器也会更简单,装饰器的属性就是构造函数的参数。还是来看怎么用类重新实现前面的 my_decorator 装饰器
 1from functools import wraps
 2
 3def my_decorator(func):
 4   
 5    @wraps(func)
 6    def wrapper(*args, **kwargs):
 7        print(f"before calling {func.__name__}")
 8        func(*args, **kwargs)
 9        print(f"after calling {func.__name__}")
10 
11    return wrapper
12 
13 
14@my_decorator
15def say_hello(firstname, lastname, **kwargs):
16    print(f"Hello {firstname} {lastname}!", kwargs)
17 
18 
19say_hello("Steve", "Jobs", company="Apple", country="USA")

用类实现装饰器

 1class MyDecorator:
 2    def __init__(self, func):
 3        self.func = func
 4    
 5    def __call__(self, *args, **kwargs):
 6        print(f"before calling {self.func.__name__}")
 7        self.func(*args, **kwargs)
 8        print(f"after calling {self.func.__name__}")
 9 
10 
11@MyDecorator         # my_decorator = MyDecorator(say_hello)
12def say_hello(firstname, lastname, **kwargs):
13    print(f"Hello {firstname} {lastname}!", kwargs)
14 
15 
16say_hello("Steve", "Jobs", company="Apple", country="USA")

执行后输出是一样的
before calling say_hello
Hello Steve Jobs! {'company': 'Apple', 'country': 'USA'}
after calling say_hello
声明 @MyDecorator 的时候会调用 __init__(self, func) 函数,并注册当前函数, 返回 MyDecorator 的一个对象。当调用 say_hello 时调用对象 my_decorator 的 __call__(self, *args, **kwargs) 函数。

带属性的类实现的装饰器

继续往前,想要创建一个 MyDecorator(101) 那样带属性装饰器,我的本能反应是在构造函数中做文章,可能是这样
1def __init__(self, func, size):
2    self.func = func
3    self.size = size

然而现在写成 @MyDecorator(101) 时就会有错了
TypeError: __init__() missing 1 required positional argument: 'size'
此路不通。正确的做法是要把 func 的传入移到 __call__() 函数中去
 1from functools import wraps
 2
 3class MyDecorator:
 4    def __init__(self, size):
 5        self.size = size
 6    
 7    def __call__(self, func):
 8        @wraps(func)
 9        def decorator(*args, **kwargs):
10            print(f"{self.size}")
11            print(f"before calling {func.__name__}")
12            func(*args, **kwargs)
13            print(f"after calling {func.__name__}")
14        return decorator
15 
16 
17@MyDecorator(101)        # my_decorator = MyDecorator(101)
18def say_hello(firstname, lastname, **kwargs):
19    print(f"Hello {firstname} {lastname}!", kwargs)
20 
21 
22say_hello("Steve", "Jobs", company="Apple", country="USA")

执行输出
101
before calling say_hello
Hello Steve Jobs! {'company': 'Apple', 'country': 'USA'}
after calling say_hello
这种加个属性就要把  func__init__() 转移到 __call__() 中的变化,有些不好理解。

我们其实可以统一写法,不管装饰器带不带属性都能采用第二种写法,构造函数中放属性,__call__() 中放被装饰的函数,就是在其中要返回一个函数

注:我们用 @wraps(func) 加注,并不会改变函数的执行行为,只是用 help(say_hello) 看到的是
Help on function say_hello in module __main__: say_hello(firstname, lastname, **kwargs)
而不会是
Help on function decorator in module __main__: decorator(*args, **kwargs)

更强类实现的 Decorator

由于类实现的 Decorator 运行期存在一个装饰器实例,可以保存状态数据,所以比单纯函数实现的装饰器更强大,我们从一个注册回调函数的例子开始。摘自网上的一个实例
 1class FunctionManager:
 2    def __init__(self):
 3        print("初始化")
 4        self.functions = []
 5        
 6    def execute_all(self):
 7        for func in self.functions:
 8            func()
 9        
10    def register(self, func):
11        self.functions.append(func)
12
13
14fm = FunctionManager()
15
16@fm.register
17def t1():
18    print("t1")
19
20@fm.register
21def t2():
22    print("t2")
23
24@fm.register
25def t3():
26    print("t3")
27
28fm.execute_all()

执行结果是
初始化
t1
t2
t3

用装饰器实现一个简陋的 REST API

现在是不是感觉和 Flask 或 FastAPI 用装饰器声明 endpoint 的实现很接近了啊,在 Flask 中我们可以这么写
1app = Flask(__name__)
2
3@app.get('/')
4def index():
5    return "hello world!"
6
7@app.get('/users')
8def list_users():
9    return "user list"

我们自己来实现一个极度简陋的 REST API Chilli
 1from http.server import HTTPServer, BaseHTTPRequestHandler
 2from urllib import parse
 3
 4class Chilli:
 5    def __init__(self):
 6        self.routes = {}
 7
 8    def get(self, path):
 9        def route_decorator(func):
10            self.routes[path] = func
11        return route_decorator
12
13    def run(self):
14        server = HTTPServer(('localhost', 8080), MyHttpHandler)
15        print('Starting server at 8080, use <Ctrl-C> to stop')
16        server.serve_forever()
17
18
19class MyHttpHandler(BaseHTTPRequestHandler):
20
21    def do_GET(self) -> None:
22        parsed_path = parse.urlparse(self.path).path
23        self.send_response(200)
24        self.send_header('Content-Type', 'text/plain; charset=utf-8')
25        self.end_headers()
26        func = app.routes[parsed_path]
27        self.wfile.write(f'{func()}\r\n'.encode('utf-8'))
28
29app = Chilli()
30
31@app.get('/')
32def index():
33    return 'hello world!'
34
35@app.get('/users')
36def users():
37    return 'user list'
38
39if __name__ == '__main__':
40    app.run()

运行后,访问 / 或 /users 的效果图如下

这里只实现了 GET, 也没有参数处理,但基本上用装饰器声明 endpoint 时有一点点模样了。

装饰器修饰类

装饰器除了修饰函数外,可以修饰类,这时候捕获的就不是 func,而是 cls
1def class_info():
2    def wrapper(cls):
3        print(f'class name {cls.__name__}')
4    return wrapper
5
6@class_info()     # Test = class_info(Test)
7class Test:
8    pass

执行输出为
class name Test

总结

最后,记住一点,能不能加上 @ 当作装饰器来用,满足的条件只要它是一个 callable 的类型,函数或实现了 __call__() 的对象。

无属性的装饰器

当装饰器为 class 时
1@MyDecorator   # 调用 __init__(self, func),返回它的对象, 假设是 my_decorator
2def say_hello(name):
3    print(f'Hello {name}')
4
5say_hello('world')  # 会调用  my_decorator.__call__(self) 函数

Python 解释到 @MyDecorator 行时会执行类的 __init__(self, func) 初始化函数,也就是会把当前函数名作为参数。然后在实际调用 say_hello() 函数时会调用 __call__() 函数

最终调用 say_hello('world') 的效果是
1MyDecorator(say_hello)('world')

当装饰器为函数时
1@my_decorator       # 调用 my_decorator(func) 函数, 假设是 wrapper
2def say_hello(name):
3    print(f'Hello {name}')
4
5say_hello('world')   # 会调用 my_decorator(func) 返回的函数 wrapper()

Python 解释到 @my_decorator 行时会调用 my_decorator(func) 函数,并要求它的返回值是 callable 类型,即还是函数(或实现了 __call__() 函数的对象)。也是在实际调用 say_hello() 函数时才会调用 my_decorator(func) 返回的函数(或实现了 __call__() 的对象)

最终调用 say_hello('world') 的效果是
1my_decorator(say_hello)('world')

留意到,我们都是在实际调用 say_hello() 函数时才会去调用 @MyDecorator 或 @my_decorator 返回的 callable 对象(函数或实现了 __call__() 函数的对象)。如果我们只用装饰器来注册信息而不真的去调用被装饰的函数,让装饰器返回 callable 对象也就不是必须的。

带属性的装饰器

当装饰器为 class 时

1@MyDecorator(size=101)   # 调用类 MyDecorator 的 __init__self, size) 函数,返回它的对象, 假设对象为 my_decorator
2def say_hello(name):
3    pass
4
5say_hello('world')     # 会调用 my_decorator.__call__(self, func) 返回的函数 wrapper()

Python 解释 @MyDecorator(size=101) 时只管装饰器的属性,而不管当前被修饰的函数,待到调用 say_hello('world') 时才有 func 的信息

最终调用 say_hello('world') 的效果是
1my_decorator(size=101)(say_hello)('world')

当装饰器为函数时
1@my_decorator(size=101)   # 调用 my_decorator(size) 函数,得到一个返回值又是函数的函数,假设是 wrapper
2def say_hello(name):
3    pass
4
5say_hello('world')   # 会调用 wrapper(func)('world')

最终调用 say_hello('world') 的效果是
1my_decorator(size=101)(say_hello)('world')

学习 Python 的装饰到现在,由最开始对 @ 符号感到既熟悉又陌生,难免会把它与 Java 的注释联系起来,其实毫无关系。随着对它的特性及实现的深入,如带属性的装饰器,被装饰的函数有参数,或有返回值的情况; 它还能装饰类,仿佛不断的越陷越深,对它的理解愈加混乱。最后把思维又往回拉,重新回到 Python 装饰器的初心上来,一切又变得豁然开朗起来。说到底 Python 的装饰器就是对函数的包装再调用,罗列上面的调用效果到一块来
MyDecorator(say_hello)('world')
my_decorator(say_hello)('world')
MyDecorator(size=101)(say_hello)('world')
my_decorator(size=101)(say_hello)('world')
上面用 MyDecorator 和 my_decorator 分别表示类和函数,my_decorator 对应的是 MyDecorator 构造函数,没有别的区别。明白了装饰器的整个调用过程,实现上哪一步是否要返回一个 callable 对象就变得一目了然了。如此理解起来,Python 的装饰器比 Java 的注解变得简单的多,Java 的注解本身不代表什么,复杂的是后面的处理器。

链接:

  1. Python Class Based Decorator with parameters that can decorate a method or a function
  2. Python decorator实现的函数注册和类decorator,python,装饰,器
  3. Python 元编程
永久链接 https://yanbin.blog/python-class-implemented-decorator/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。