Python中的函数装饰器和闭包原理
本文为《流畅的Python》——函数装饰器和闭包的学习笔记和总结
文章目录
函数装饰器和闭包
1.1 装饰器概述
-
装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。可以返回原函数,也可以返回装饰器内部定义的函数。
@decorate def target(): print('running target()')
下边写法与上述代码效果一样:
def target(): print('running target()') target = decorate(target)
1.2 装饰器与被装饰对象执行顺序
-
装饰器的一个关键特性:在被装饰的函数定义之后,装饰器会立即运行。通常是在导入时(即Python加载模块时)。
自定义一个注册模块
registration.py
:registry = [] 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()
运行结果:
$ 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()
-
if __name__=='__main__':
相当于导入当前模块,可以发现在执行main()
之前有两个函数被装饰,所以装饰器就执行了两次。 -
也可以通过
import registration
查看:>>> import registration running register(<function f1 at 0x10063b1e0>) running register(<function f2 at 0x10063b268>)
-
-
装饰器通常在一个模块中定义,在另一个模块中导入去装饰函数。
-
多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返
回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。
1.3 变量作用域规则
-
函数中变量分为局部变量和全局变量,如果在函数中修改全局变量,又没有使用
global
关键字修饰为全局变量,就会报错。>>> b = 6 # b为全局变量 >>> def f1(a): # a为局部变量 ... print(a) ... print(b) >>> f1(3) 3 6
如果在函数内部修改b的值,又不声明b为全局变量或者为局部变量就会报错:
>>> 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
正确的做法是使用
global
关键字:>>> b = 6 >>> def f3(a): ... global b ... print(a) ... print(b) ... b = 9 ... >>> f3(3) 3 6 >>> b 9
1.4 闭包
-
当使用函数嵌套时就使用到了闭包,闭包指延伸了作用域的函数,其中包含函数体中引用、但是不在定义体中定义的非全局变量。
-
比如下边的高阶函数(以函数作为参数或者返回值的函数),
series = []
对于make_averager
函数来说是一个局部变量。# 计算移动平均值的高阶函数 def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager
当我们调用
make_averager
函数时返回averager
函数对象,然后我们使用averager
对象传参,此时由于Python垃圾回收机制会将make_averager
函数中的局部变量series
回收,因为已经没有引用指向它了。但是
averager
却可以继续往既不是全局变量又不是局部变量的series
中添加值,此时的series
就是*变量,这就是利用了闭包的原理。>>> avg = make_averager() >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
- 所以,闭包是一种函数,它会保留定义函数时存在的*变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
1.5 nonlocal声明
在1.4节中的make_averager
函数中的*变量series
是一个可变类型,如果在闭包中对不可变类型的*变量进行更改,就会抛出异常:
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
>>>
这是因为对不可变类型进行修改Python会隐式的创建一个局部变量来保存修改后的值,比如上边的count += 1
等价于count = count + 1
而此时的count
已经不再是*变量而是局部变量了,此时就会报错count
未定义。
可以使用nonlocal
关键字来声明为*变量。
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
1.6 自定义装饰器
1.6.1 不带参数的装饰器
如果直接使用闭包的方式创建一个装饰器,将会遮盖被装饰函数的__name__
和__doc__
属性, 使用functools.wraps装饰器可以把属性从被装饰函数复制到闭包函数中。
如:自定义装饰器 :在每次调用被装饰的函数时计时,然后把经过的时间、
传入的参数和调用的结果打印出来。
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
return result
return clocked
1.6.2 带参数的装饰器
带有参数的装饰器:
registry = set()
# 注意此时的register并不是一个真正的装饰器而是一个装饰器工厂函数,
# 调用它会返回真正的装饰器decorate
def register(active=True):
def decorate(func):
# 可以在装饰器中接收active参数
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)并不是一个装饰f1函数的装饰器,
# 返回的结果是一个装饰器
@register(active=False)
def f1():
print('running f1()')
# 所以即使不传递参数也要加(),是因为这里是函数调用
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
如果不使用@
语法 ,那就要像常规函数那样使用 register
;若想把 f
添加到 registry
中,则装饰f
函数的句法是register()(f)
;不想添加(或把它删除)的话,句法是register(active=False)(f)
:
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry
{<function f2 at 0x10073c268>}
>>> register()(f3)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2)
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry
{<function f3 at 0x10073c158>}
带有参数的装饰器,通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套:因为利用的是工厂函数的设计模式。
1.7 标准库中的装饰器
functools
模块中的三个装饰器:
-
functools.wraps
:协助构建行为良好的装饰器 -
functools.lru_cache
: 把耗时的函数的结果保存起来,避免传入相同的参数时重复计算 ,缓存不是永久缓存当一段时间不使用就会被扔掉。使用前边的clock装饰器来装饰一个斐波那契数函数:
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.00000095s] fibonacci(0) -> 0 [0.00000095s] fibonacci(1) -> 1 [0.00007892s] fibonacci(2) -> 1 [0.00000095s] fibonacci(1) -> 1 [0.00000095s] fibonacci(0) -> 0 [0.00000095s] fibonacci(1) -> 1 [0.00003815s] fibonacci(2) -> 1 [0.00007391s] fibonacci(3) -> 2 [0.00018883s] fibonacci(4) -> 3 [0.00000000s] fibonacci(1) -> 1 [0.00000095s] fibonacci(0) -> 0 [0.00000119s] fibonacci(1) -> 1 [0.00004911s] fibonacci(2) -> 1 [0.00009704s] fibonacci(3) -> 2 [0.00000000s] fibonacci(0) -> 0 [0.00000000s] fibonacci(1) -> 1 [0.00002694s] fibonacci(2) -> 1 [0.00000095s] fibonacci(1) -> 1 [0.00000095s] fibonacci(0) -> 0 [0.00000095s] fibonacci(1) -> 1 [0.00005102s] fibonacci(2) -> 1 [0.00008917s] fibonacci(3) -> 2 [0.00015593s] fibonacci(4) -> 3 [0.00029993s] fibonacci(5) -> 5 [0.00052810s] fibonacci(6) -> 8 8
因为是递归所以有很多参数相同结果相同的重复调用,导致程序运行时间长。
使用
lru_cache
装饰后,性能会显著提升:
import functools
from clockdeco import clock
# 注意这个装饰器使用必须带括号,因为functools.lru_cache()不是装饰器
# 其返回的对象是个装饰器,这里只是函数调用
@functools.lru_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.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8
其中 lru_cache
可以使用两个可选的参数来配置 functools.lru_cache(maxsize=128, typed=False)
:
-
maxsize
参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize
应该设为 2 的幂。 -
typed
参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。
注:被lru_cache装饰的函数的参数必须是可散列的。