函数装饰器 顾名思义就是装饰函数,装饰器需要用另外一个函数作为参数并且返回一个可调用类型:
@decorate
def target():
print('running target()')
等同于
def target():
print('running target()')
target = decorate(target)
这个例子里面装饰器把函数替换成了另一个函数:
>>> def deco(func):
... def inner():
... print('running inner()')
... return inner
...
>>> @deco
... def target():
... print('running target()')
...
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x10063b598>
装饰器就是语法糖,本身没有任何功能的提升,不用装饰器也能实现所有装饰器实现了的功能。
装饰器的三大特点:
- 装饰器是一个函数或者可被调用对象
- 装饰器可以用另一个函数替换装饰的函数
- 装饰器在模块加载时执行
最后一点比较重要,我们来检验一下:
registry = []
def register(func):
print(f'running register({func})')
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
结果十分合理:
$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()
哪怕是从别的地方 import 这个模块:
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)
实际写代码的时候,装饰器的应用跟上面那个例子有些出入:
- 装饰器一般定义在一个模块,然后应用在另外一个模块
- 装饰器大部分在内部会定义一个函数然后返回(闭包)
讨论闭包前必须先说一下变量的范围规则,函数有局部变量,如果在函数内部没有定义的变量,会把它当成全局变量看待:
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined
再看一个好玩的例子:
>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
这里因为编译 f2 这个函数的时候,看到了 b 在函数内被赋值了,所以将b看待成一个局部变量,然后执行到print(b), 这一步,发现 局部变量 b 的范围不包括这里,所以报错。
修改成这样,b就是全局变量了
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
闭包
函数里面套着一个函数
现在这里有一个计算平均值的类:
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
每次调用都会计算之前调用过的所有数的平均值,这里面因为 这个类 里 有保存一个列表,记录着所有数据
这里再仿照这个类的行为用函数来实现
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(15)
12.0
这个函数保存数据的位置在这里:
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
再看一个说明如何使用 nonlocal 的例子:
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
运行却会报错
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
这里因为 count 或者其他的不可变类型 再 averager 函数下面被赋值,所以编译的时候会将其认定成这个函数的局部变量,但是在外层也已经有了 count 变量,这时候需要 nonlocal 关键词 让其变成一个 free variable,之前的 list 的例子没有问题因为 list 的那些操作只是引用,没有对其赋值
改完后:
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
变量查找的逻辑:
- 如果是 global x 这样声明的,那么 x 就是这个模块的全局变量
- 如果是 nonlocal x 这样声明的,那么x属于这个函数外层第一次定义的方法
- 如果x是函数的参数,然后被赋值了,那么x 是这个函数的局部变量
- 如果 x 只是引用,没有赋值,也不是参数 ,那么 会根据 nonlocal 的规则查找 x,然后再根据 global 的规则去 找,最后会去 __buitins__.__dict__找
这里来实现一个简单的装饰器:
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked
import time
from clockdeco0 import clock
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720
这时候,factorial 的 __name__属性就会变成:
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>
然而,我们并不想这样,因为如果不同函数被同一个装饰器装饰,那么它们的 __name__, __doc__ 属性都会被 装饰器覆盖,这时候需要 用 functools.wraps 这个 装饰器去把原函数的属性复制过来。
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = [repr(arg) for arg in args]
arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
arg_str = ', '.join(arg_lst)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked
还有一些比较典型的装饰器:
第一个是 functools.cache,这个是对内存优化处理的,在计算被装饰函数用同样的参数时会保留结果:
from clockdeco import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8
可以看到 fibonacci(0) 啥的被调用了很多次,如果加了这个装饰器,看效果:
import functools
from clockdeco import clock
@functools.cache
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8
嵌套装饰器是从上到下对应从左到右的顺序:
@alpha
@beta
def my_fn():
...
等于
my_fn = alpha(beta(my_fn))
functools.lru_cache 说明了装饰器也可以添加参数:
@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
...
functools.singledispatch 这个装饰器可以处理同一个函数的不同类型参数:
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch
def htmlize(obj: object) -> str:
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
@htmlize.register
def _(text: str) -> str:
content = html.escape(text).replace('\n', '<br/>\n')
return f'<p>{content}</p>'
@htmlize.register
def _(seq: abc.Sequence) -> str:
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
@htmlize.register
def _(n: numbers.Integral) -> str:
return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register
def _(n: bool) -> str:
return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction)
def _(x) -> str:
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@htmlize.register(decimal.Decimal)
@htmlize.register(float)
def _(x) -> str:
frac = fractions.Fraction(x).limit_denominator()
return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
带参数的装饰器怎么写:
先来个不带参数的:
registry = []
def register(func):
print(f'running register({func})')
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
这个是带参数的版本:
registry = set()
def register(active=True):
def decorate(func):
print('running register'
f'(active={active})->decorate({func})')
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
其实就是在原来的装饰器函数外层再套一个函数,这个函数返回的是原装饰器函数,然后这个函数的参数就是装饰器的参数。