函数装饰器用于在代码中“标记”函数,以某种方式增强函数的行为。
想理解与掌握这一功能必须先理解闭包;除此之外,闭包还是回调式异步编程和函数式编程风格的基础
装饰器#
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把装饰后的函数返回,或者将其替换成另一个函数或可调用对象,然后返回。python也支持类装饰器。
1
2
3
4
5
6
7
8
9
10
11
12
|
def deco(func):
def inner():
print('running inner()...')
return inner
@deco
def target():
print('running target()...')
target()
>>>running inner()...
print(target)
>>><function deco.<locals>.inner at 0x7f63096898c8>
|
可见target
现在是对inner
的引用
python何时执行装饰器#
装饰器的一个关键特性是,它们在被装饰的函数定义后立即执行。这通常是在导入时
(即python加载模块时)
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
36
|
registry = list()
def register(func):
print('running register(%s)' % 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()
>>>running register(<function f1 at 0x7f30252f0620>)
>>>>>>running register(<function f2 at 0x7f30252f08c8>)
>>>running main()
>>>registry -> [<function f1 at 0x7f30252f0620>, <function f2 at 0x7f30252f08c8>]
>>>running f1()...
>>>running f2()...
>>>running f3()...
|
变量作用域规则#
一个有意思的例子
1
2
3
4
5
6
7
8
9
|
b = 6
def fun(a):
print(a)
print(b)
b = 7
fun(3)
>>>3
>>>UnboundLocalError: local variable 'b' referenced before assignment
|
产生这种现象的原因是:python假定在函数定义体中赋值的变量是局部变量
使用global
关键字可以解决这一问题
1
2
3
4
5
6
7
8
9
10
|
b = 6
def fun(a):
global b
print(a)
print(b)
b = 7
fun(3)
>>>3
>>>6
|
闭包指延伸了作用域的函数,其中包含函数定义体中引用
、但是不在定义体中定义的非全局变量
。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
def make_averager():
series = list()
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
avg = make_averager()
print(avg(10))
>>>10.0
print(avg(11))
>>>10.5
print(avg(12))
>>>11.0
|
series
是make_averager
函数的局部变量,因为那个函数的定义体中初始化了series=list()
。注意,调用avg(10)
时,make_averager
函数已经返回了,而它的本地作用域也一去不复返了。在averager
函数中,series
是自由变量(free variable)
,指未在本地作用域中绑定的变量。

averager
的闭包延伸到那个函数的作用域之外,包含自由变量series
的绑定
利用__code__
属性(表示编译后的函数定义体中保存局部变量和自由变量的名称)审查make_averager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
def make_averager():
series = list()
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
avg = make_averager()
avg(10)
avg(11)
avg(12)
print(avg.__code__.co_varnames)
>>>('new_value', 'total')
print(avg.__code__.co_freevars)
>>>('series',)
print(avg.__closure__)
>>>(<cell at 0x7f647d0c6cd8: list object at 0x7f647d02e9c8>,)
print(avg.__closure__[0].cell_contents)
>>>[10, 11, 12]
|
series
的定在返回的avg
函数的__closure__
属性中。avg.__closure__
中的各个元素对应于avg.__code__.co_freevars
中的一个名称。这些元素是cell
对象,有个cell_contents
属性,保存着真正的值
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量
nonlocal
声明#
一个有问题的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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)
>>>UnboundLocalError: local variable 'count' referenced before assignment
|
问题和之前遇到的一样,当count
是数字或任何不可变类型时,count += 1
语句的作用其实与count
= count + 1
一样,所以count
会被认为是局部变量,而不是外部变量
使用nonlocal
解决上述问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
avg = make_averager()
print(avg(10))
>>>10.0
print(avg(11))
>>>10.5
print(avg(12))
>>>11.0
|
python2中没有nonlocal
,只能使用可变对象进行处理
实现一个简单的装饰器(函数运行计时器)#
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
|
import time
def clock(func):
def clocked(*args):
start = time.perf_counter()
res = func(*args)
end = time.perf_counter()
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
return res
return clocked
@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n-1)
if __name__ == '__main__':
print('6! =', factorial(6))
>>>[0.00000067s] factorial(1) -> 1
>>>[0.00004895s] factorial(2) -> 2
>>>[0.00006290s] factorial(3) -> 6
>>>[0.00008678s] factorial(4) -> 24
>>>[0.00009936s] factorial(5) -> 120
>>>[0.00011125s] factorial(6) -> 720
>>>6! = 720
print(factorial.__name__)
>>>clocked
|
上面的clock
饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name__
和__doc__
属性。
使用functools.wraps
装饰器可以把相关的属性从func
复制到clocke
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import time
from functools import wraps
def clock(func):
@wraps(func)
def clocked(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
return res
return clocked
@clock
def factorial(n):
return 1 if n<2 else n*factorial(n-1)
print(factorial.__name__)
>>>factorial
|
此外,使用functools.wraps
还能正确处理关键字参数kwargs
。
标准库中的装饰器#
python内置了三个用于装饰方法的装饰器:@property
, @classmethod
, @staticmethod
除了上面提到的functools.wraps
,还有functools.lru_cache
和singledispatch
functools.lru
实现了记忆化的功能,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算(lrh:Least Recently Used), 缓存不会无限增长,一段时间不用的缓存条目会被扔掉
使用生成n个斐波那切数
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import time
from functools import wraps, lru_cache
def clock(func):
@wraps(func)
def clocked(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
return res
return clocked
@clock
def naive_fibonacci(n):
if n < 2:
return n
return naive_fibonacci(n-2) + naive_fibonacci(n-1)
@lru_cache()
@clock
def lru_fibonacci(n):
if n < 2:
return n
return lru_fibonacci(n-2) + lru_fibonacci(n-1)
print(naive_fibonacci(6))
[0.00000058s] naive_fibonacci(0) -> 0
[0.00000077s] naive_fibonacci(1) -> 1
[0.00009734s] naive_fibonacci(2) -> 1
[0.00000043s] naive_fibonacci(1) -> 1
[0.00000098s] naive_fibonacci(0) -> 0
[0.00000061s] naive_fibonacci(1) -> 1
[0.00003599s] naive_fibonacci(2) -> 1
[0.00007170s] naive_fibonacci(3) -> 2
[0.00020467s] naive_fibonacci(4) -> 3
[0.00000045s] naive_fibonacci(1) -> 1
[0.00000046s] naive_fibonacci(0) -> 0
[0.00000053s] naive_fibonacci(1) -> 1
[0.00003116s] naive_fibonacci(2) -> 1
[0.00006405s] naive_fibonacci(3) -> 2
[0.00000046s] naive_fibonacci(0) -> 0
[0.00000054s] naive_fibonacci(1) -> 1
[0.00004143s] naive_fibonacci(2) -> 1
[0.00000042s] naive_fibonacci(1) -> 1
[0.00000059s] naive_fibonacci(0) -> 0
[0.00000080s] naive_fibonacci(1) -> 1
[0.00003937s] naive_fibonacci(2) -> 1
[0.00007146s] naive_fibonacci(3) -> 2
[0.00014021s] naive_fibonacci(4) -> 3
[0.00022997s] naive_fibonacci(5) -> 5
[0.00046144s] naive_fibonacci(6) -> 8
8
print(lru_fibonacci(6))
[0.00000024s] lru_fibonacci(0) -> 0
[0.00000029s] lru_fibonacci(1) -> 1
[0.00001679s] lru_fibonacci(2) -> 1
[0.00000151s] lru_fibonacci(3) -> 2
[0.00003300s] lru_fibonacci(4) -> 3
[0.00000093s] lru_fibonacci(5) -> 5
[0.00004966s] lru_fibonacci(6) -> 8
8
|
lru_cache
以使用两个可选的参数来配置
1
|
functools.lru_cache(maxsize=128, typed=False)
|
maxsize
数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize
应该设为2的幂
。typed
参数如果设为True
,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为lru_cache
使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache
装饰的函数,它的所有参数
都必须是可散列
的。
单分派泛函数#
python不支持重载(支持重写),使用functools.singledispatch
装饰器可以实现类似于重载的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str)
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
|
singledispatch
机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数
叠放装饰器#
1
2
3
4
|
@d1
@d2
@def f():
pass
|
等价于
1
2
3
|
def f():
pass
f = d1(d2(f))
|
参数化装饰器#
创建含参数装饰器的方法:创建一个装饰器工厂函数,把参数传给它,返回一个
装饰器,然后再把它应用到要装饰的函数上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
registry = set()
def register(active=True):
def decorate(func):
print('running register(active=%s) -> decorate(%s)') % (active, func)
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()...')
@register(active=True)
def f2():
print('running f2()...')
def f3():
print('running f3()...')
|
注意区别,这个例子里最外层的函数的参数不是函数.通常含有参数的装饰器有三层
参数化clock装饰器#
为clock
装饰器添加一个功能,允许用户传入一个格式字符串,控制被装饰函数的输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({arg_str}) -> {res}'
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*args):
start = time.time()
res = func(*args)
end = time.time()
elapsed = end - start
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(fmt.format(**locals()))
return res
return clocked
return decorate
@clock()
def my_sleep():
time.sleep(2)
my_sleep()
|
Graham Dumpleton 和 Lennart Regebro(本书的技术审校之一)认为,装饰器最好通过实现 call 方法的类实现,不应该像本章的示例那样通过函数实现
本章延展阅读部分有很多关于装饰器的资料推荐