基本概念
- 函数可以作为参数传递的语言,可以使用装饰器
- 装饰器是一个函数(或类),接一个函数(或类),返回一个函数(或类)
- 装饰器不改变被装饰函数,可以拓展或修改被装饰函数的功能
一些用法
- 打 log,debug
- 访问控制
- 计数,计时,api 调用限速
- 人为增加延迟
- 动态注册组件
- 缓存调用结果
- …
引入
Py 函数是一等对象,可以作为参数传给另一个函数
1
2
3
4
5
6
7
8
9
10
11
12
def say_hello(name):
    return f"Hello {name}"
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"
def greet_bob(greeter_func):
    return greeter_func("Bob")
greet_bob(say_hello) # 'Hello Bob'
greet_bob(be_awesome) # 'Yo Bob, together we are the awesomest!'
在函数中定义函数
1
2
3
4
5
6
def parent():
    def child():
        print("child")
    return child
parent() # <function parent.<locals>.child at 0x7fe1c6921fc0>
Python 的 for 不引入作用域,整段代码除了 power 里面都是 global
1
2
3
4
5
6
7
8
9
10
11
12
13
14
powers = []
for i in range(3):
    def power(x):
        # i = i+1 # UnboundLocalError: local variable 'i' referenced before assignment
        return x ** i
    powers.append(power)
print(i) # 2
for p in powers:
    print(p(2))
# 4
# 4
# 4
wrapper 函数叫做闭包(保存一个函数和其运行环境)。闭包可以保存运行环境,变量绑定参数之后值一直不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
powers = []
for i in range(3):
    def maker(ic): # 参数叫i也一样,局部覆盖全局
        def power(x):
            return x ** ic
        return power
    powers.append(maker(i))
print(i) # 2
for p in powers:
    print(p(2))
# 1
# 2
# 4
绑定参数就可以固定变量指
1
2
3
4
5
6
7
8
9
10
11
12
13
powers = []
for i in range(3):
    def power(x, i=i):
        return x ** i
    powers.append(power)
print(i) # 2
for p in powers:
    print(p(2))
# 1
# 2
# 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
def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before")
        func(*args, **kwargs)
        print("After")
    return wrapper
def add(a, b):
    print(a+b)
'''
1. 调用装饰器函数,返回装饰过的函数
- 可以保留未装饰函数
'''
add = decorator(add)
print(add) # <function decorator.<locals>.wrapper at 0x7fe1c6922290>
add(2, 3)
'''
2. @
- 在定义时立即装饰
    - 递归也是装饰过的
    - 没有未被装饰的函数
'''
@decorator
def add(a, b):
    print(a+b)
print(add) # <function decorator.<locals>.wrapper at 0x7fe1c6922050>
add(2, 3)
注意
- 不要忘了返回 wrapper
- 注意区分外层函数参数和内层函数参数
装饰后,函数的一些元数据,比如name,doc之类的会变成 wrapper 的,用 wraps 工具把被装饰函数的属性贴到装饰过的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Before")
        func(*args, **kwargs)
        print("After")
    return wrapper
def add(a, b):
    print(a+b)
print(add) # <function add at 0x7f5d9deedfc0>
add = decorator(add)
print(add) # <function add at 0x7f5d9deee050> 不是同一个函数
add(2, 3)
叠加
装饰器可以叠加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def p(func):
    def wrapper(s):
        return "<p>" + func(s) + "</p>"
    return wrapper
def div(func):
    def wrapper(s):
        return "<div>" + func(s) + "</div>"
    return wrapper
@div
@p
def say(something):
    return something
say("hello") # '<div><p>hello</p></div>'
- 相当于 div(p(say))
- 每层装饰都是函数调用,可能有性能影响
装饰类的方法
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
class Circle:
    def __init__(self, radius):
        self._radius = radius
    @property
    def radius(self):
        """Get value of radius"""
        return self._radius
    @radius.setter
    def radius(self, value):
        """Set radius, raise error if negative"""
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius must be positive")
    @property
    def area(self):
        """Calculate area inside circle"""
        return self.pi() * self.radius**2
    def cylinder_volume(self, height):
        """Calculate volume of cylinder with circle as base"""
        return self.area * height
    @classmethod
    def unit_circle(cls):
        """Factory method creating a circle with radius 1"""
        return cls(1)
    @staticmethod
    def pi():
        """Value of π, could use math.pi instead though"""
        return 3.1415926535
装饰类本身
1
2
3
4
5
6
from dataclasses import dataclass
@dataclass
class PlayingCard:
    rank: str
    suit: str
装饰器接参数
- @函数()的话,函数只会接到()里的参数,函数应该返回一个装饰器函数
- @函数 的话,函数只会接到被装饰函数
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
def repeat(times=2):
    print("times", times)
    def decorator(func):
        def wrapper():
            for _ in range(times):
                func()
        return wrapper
    return decorator
@repeat(times=3)
def hello():
    print("hello")
# times 3
hello()
# hello
# hello
# hello
@repeat
def hi():
    print("hi")
# times <function hi at 0x7fd774eedfc0> 不带()调用,被装饰函数被当作times传进去了
hi() # TypeError: repeat.<locals>.decorator() missing 1 required positional argument: 'func'
让装饰器同时支持 @装饰器 和 @装饰器() 调用方法
TODO: 为什么要 _ ,没有也能跑 def repeat(_func=None, _,times=2):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def repeat(_func=None, times=2):
    def decorator(func):
        def wrapper():
            for _ in range(times):
                func()
        return wrapper
    if _func is None:
        return decorator
    else:
        return decorator(_func)
@repeat
def hi():
    print("hi")
hi()
@repeat(times=3)
def hello():
    print("hello")
hello()
带状态的装饰器
函数属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def counter(func):
    def wrapper():
        wrapper.num_called += 1
        print(f"Calling the {wrapper.num_called} time")
        return func()
    wrapper.num_called = 0
    return wrapper
@counter
def func():
    pass
func() # Calling the 1 time
func() # Calling the 2 time
print(func.num_called) # 2
func() # Calling the 3 time
装饰器类
__init__接被装饰函数,__call__允许实例被调用。调用这个装饰器的返回是类的一个实例,所以call就是 wrap 函数,不应该返回 wrap 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from functools import update_wrapper
class Counter:
    def __init__(self, func):
        update_wrapper(self, func)
        self._func = func
        self._count = 0
    def __call__(self):
        self._count += 1
        print(f"Calling the {self._count} time")
        self._func()
@Counter
def func():
    pass
func()
装饰器实现 singleton
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def singleton(cls):
    def wrapper():
        if wrapper.instance is None:
            wrapper.instance = cls()
            return wrapper.instance
        return wrapper.instance
    wrapper.instance = None
    return wrapper
class Hi:
    pass
Hi() # <__main__.Hi object at 0x7f4ad4b151b0>
Hi() # <__main__.Hi object at 0x7f4ad4b15e40>
@singleton
class Hello:
    pass
Hello() # <__main__.Hello object at 0x7f4ad4b15ae0>
Hello() # <__main__.Hello object at 0x7f4ad4b15ae0>
缓存调用结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def cacher(func):
    def wrapper(*args, **kwargs):
        params = args + tuple(kwargs.items())
        wrapper.num_call += 1
        if params in wrapper.cache:
            return wrapper.cache[params]
        else:
            wrapper.num_calc += 1        
            wrapper.cache[params] = func(*args, **kwargs)
        return wrapper.cache[params]
    wrapper.cache = {}
    wrapper.num_call = 0
    wrapper.num_calc = 0
    return wrapper
@cacher
def fib(idx):
    if idx < 2:
        return idx
    return fib(idx-1 ) + fib(idx-2)
fib(10)
print(fib.num_call) # 19
print(fib.num_calc) # 11
使用functool内置LRU cache
1
2
3
4
5
6
7
8
9
10
import functools
@functools.lru_cache(maxsize=5)
def fib(idx):
    if idx < 2:
        return idx
    return fib(idx-1 ) + fib(idx-2)
fib(10)
fib.cache_info() # CacheInfo(hits=8, misses=11, maxsize=5, currsize=5)
!r repr !s str !a ascii
time.perf_counter()
https://realpython.com/primer-on-python-decorators/
https://peps.python.org/pep-0318/
https://wiki.python.org/moin/PythonDecorators
https://wiki.python.org/moin/PythonDecoratorLibrary
https://github.com/micheles/decorator
