第7章–函数装饰器和闭包
本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的
参数化装饰器。但是,在实现这一目标之前,我们要讨论下述话题:
• Python 如何计算装饰器句法
• Python 如何判断变量是不是局部的
• 闭包存在的原因和工作原理
• nonlocal 能解决什么问题
掌握这些基础知识后,我们可以进一步探讨装饰器:
• 实现行为良好的装饰器
• 标准库中有用的装饰器
• 实现一个参数化装饰器
7.1装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数,装饰器可能会处理被装饰的函数,并返回,或者将其替换成另一个函数和对象
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 0x0000023F71D96730>
7.2python何时执行装饰器
在被装饰的函数定义之后装饰器立即执行,这通常是在导入模块时
def deco(func):
print('running decorator of ',func)
return func
@deco
def f1():
print('running f1()')
@deco
def f2():
print('running f2()')
def f3():
print('running f3()')
if __name__=='__main__':
print('start to run main')
f1()
f2()
f3()
# running decorator of <function f1 at 0x00000251DDD066A8>
# running decorator of <function f2 at 0x00000251DDD06730>
# start to run main
# running f1()
# running f2()
# running f3()
查看输入可以看到在run main之前修饰器已经被执行两次,具体的函数执行只有在调用的时候才会执行,如果我们把上面代码作为模块导入,输出如下,就算我们还没有调用任何函数,装饰器也已经被执行了。
running decorator of <function f1 at 0x000002013C367158>
running decorator of <function f2 at 0x000002013C3672F0>
7.4 变量作用域规则
b=3
def f(a):
print(a) # 6
print(b) # 3
f(6)
b=3
def f(a):
print(a) # 6
print(b) # UnboundLocalError: local variable 'b' referenced before assignment
b=9
f(6)
正常来说,如果在函数局部内找不到变量,会尝试寻找全局变量,第一处代码就是这样,但第二处代码会报错,原因是python编译函数的定义体时,会判断b是全局变量,而print(b)时局部b还没有赋值,所以报错,具体可以用dis包看一下字节码比较
7.5 闭包
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的
非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量
def outer(b):
def inner(a):
print(a+b)
return inner
fun=outer(2)
fun(5) # 7
fun(7) # 9
print(fun.__code__.co_varnames) # ('a',)
print(fun.__code__.co_freevars) # ('b',)
print(fun.__closure__[0].cell_contents) # 输出2 是自由变量b的值
fun1=outer(3)
fun1(5) # 8
fun1(7) # 10
代码中inner函数是outer的内部函数,访问了不在inner定义体内的变量b
其中b是自由变量(free variable)指未在本地作用域绑定的变量
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,
虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
7.6 nonlocal声明
def outer(b):
def inner(a):
b+=1 # UnboundLocalError: local variable 'b' referenced before assignment
print(a+b)
return inner
fun=outer(2)
fun(5)
fun(7)
自由变量b是不可变对象,b+=1相当于b=b+1 此时在inner内创建了局部变量b,所以b不在是自由变量,b+1自然会报错,因为还不存在b。
要想明确表示b是自由变量,用nonlocal关键字
def outer(b):
def inner(a):
nonlocal b
b+=1
print(a+b)
return inner
fun=outer(2)
fun(5) # 8
fun(7) # 11
7.7 实现一个简单的修饰器
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=[]
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
@clock
def add(a,b):
'''return a+b'''
return a+b
def add1(a,b):
return a+b
add(10,11) # [0.00000080s] add(10, 11) -> 21
add1=clock(add1)
add1(10,11) # [0.00000060s] add1(10, 11) -> 21
print(add.__name__)
print(add.__doc__)
# 如果不用@functools.wraps(func),后两行输出是
# clocked
# None
# 如果用@functools.wraps(func),后两行输出是
# add
# return a+b
add跟add1的表现是一样的,可以解释修饰器的实际行为
用简单修饰器的话会有被装饰函数的__name__和__doc__被覆盖的问题
functools.wraps可以解决被装饰函数name跟doc被覆盖的问题,详见代码
7.8 标准库中的装饰器
preperty、classmethod、staticmethod后续章节会介绍
functools.lru_cache
实现了函数缓存功能,把耗时的函数结果保存起来,避免传入相同的参数时重新计算,函数签名是
functools.lru_cache(maxsize=128,typed=False)
maxsize应该设为2 的幂,typed=True会把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数区分开,lru_cache用字典存储结果,key值根据函数的参数来创建,所以被lru_cache修饰的函数,所有的参数都必须是hashable的
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=[]
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
@functools.lru_cache()
@clock
def fibonacci(n):
if n<2:
return n
return fibonacci(n-2)+fibonacci(n-1)
print(fibonacci(6))
# 不用lru_cache的情况
# [0.00000040s] fibonacci(0) -> 0
# [0.00000040s] fibonacci(1) -> 1
# [0.00011190s] fibonacci(2) -> 1
# [0.00000040s] fibonacci(1) -> 1
# [0.00000040s] fibonacci(0) -> 0
# [0.00000100s] fibonacci(1) -> 1
# [0.00038020s] fibonacci(2) -> 1
# [0.00043690s] fibonacci(3) -> 2
# [0.00061020s] fibonacci(4) -> 3
# [0.00000040s] fibonacci(1) -> 1
# [0.00000030s] fibonacci(0) -> 0
# [0.00000030s] fibonacci(1) -> 1
# [0.00001300s] fibonacci(2) -> 1
# [0.00002540s] fibonacci(3) -> 2
# [0.00000030s] fibonacci(0) -> 0
# [0.00000030s] fibonacci(1) -> 1
# [0.00001170s] fibonacci(2) -> 1
# [0.00000020s] fibonacci(1) -> 1
# [0.00000040s] fibonacci(0) -> 0
# [0.00000030s] fibonacci(1) -> 1
# [0.00001310s] fibonacci(2) -> 1
# [0.00004360s] fibonacci(3) -> 2
# [0.00006670s] fibonacci(4) -> 3
# [0.00013560s] fibonacci(5) -> 5
# [0.00077930s] fibonacci(6) -> 8
# 8
#使用lru_cache的情况
# [0.00000050s] fibonacci(0) -> 0
# [0.00000050s] fibonacci(1) -> 1
# [0.00004830s] fibonacci(2) -> 1
# [0.00000080s] fibonacci(3) -> 2
# [0.00006260s] fibonacci(4) -> 3
# [0.00000060s] fibonacci(5) -> 5
# [0.00007560s] fibonacci(6) -> 8
# 8
funtools.singledispatch
singledispatch可以实现python下的函数’重载’,这种’重载’支持对用相同的参数签名但不同的参数类型进行不同的函数实现,注意:singledispatch是根据函数的第一个参数类型来判断执行对应函数
下面代码中用_来表示无关紧要的函数命名,singledispatch支持多个register装饰器,让一个函数支持多个类型
from functools import singledispatch
@singledispatch
def output(obj):
print('obj',repr(obj))
@output.register(str)
def _(s):
print('str',s)
@output.register(int)
def _(num):
print('int',num)
@output.register(dict)
@output.register(list)
def _(lst):
print('list or dict',lst)
output(object())
output(1)
output('1')
output([1])
output({1:1})
# obj <object object at 0x000002B9A85DB0C0>
# int 1
# str 1
# list or dict [1]
# list or dict {1: 1}
7.9叠放装饰器
下面两段代码行为是一致的
@d1
@d2
def f():
pass
def f():
pass
f=d1(d2(f))
7.10参数化装饰器
三层次装饰器
import time
import functools
def clock(output_result=True):
def decorate(func):
def clocked(*args,**kwargs):
t0=time.perf_counter()
result=func(*args,**kwargs)
elapsed=time.perf_counter()-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)
if output_result:
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
else:
print('[%0.8fs] %s(%s)' % (elapsed, name, arg_str))
return result
return clocked
return decorate
@clock(output_result=True)
def add(a,b):
'''return a+b'''
return a+b
@clock(output_result=False)
def add1(a,b):
return a+b
add(1,2)
add1(1,2)
# [0.00000090s] add(1, 2) -> 3
# [0.00000070s] add1(1, 2)
延伸阅读
wrapt库可以简化装饰器和动态函数包装器的实现
介绍:A Python module for decorators, wrappers and monkey patching.
https://wrapt.readthedocs.io/en/latest/
decorator库旨在“简化普通程序员使用装饰器的方式,并且通过各种复杂的示例推广装饰器”
https://pypi.org/project/decorator/
reg支持多分派泛函数(根据多个定位参数来进行函数重载)
https://reg.readthedocs.io/en/latest/