基本概念
- 函数可以作为参数传递的语言,可以使用装饰器
- 装饰器是一个函数(或类),接一个函数(或类),返回一个函数(或类)
- 装饰器不改变被装饰函数,可以拓展或修改被装饰函数的功能
一些用法
- 打 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