什么是装饰器
装饰器是一个可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会:
1,处理被装饰的函数,然后把它返回
2,将其替换成另一个函数或者对象
若有个名为decorate的装饰器,则:
@decorate def target(): print('running target()')
等价于:
def target(): print('running target()') target = decorate(target)
上述两种写法结果一样,函数执行完之后得到的target不一定是原来那个target()函数,而是decorate(target)返回的函数。
确认被装饰的函数会替换成其他函数的一个示例:
def deco(func): def inner(): print('running inner()') return inner #函数deco返回inner对象 @deco #使用deco装饰target def target(): print('running target()') target() #调用target,运行inner print(target) #target时是inner的引用
如下结果,target被替换掉了,它是inner的引用。
running inner()
<function deco.<locals>.inner at 0x00000253D76B8A60>
严格来说,装饰器只是语法糖。装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。
装饰器两大特性:
1,能把被装饰的函数替换成其他函数(如前所示)
2,加载模块时立即执行
python执行装饰器时机(加载模块时)
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()
如上,f1()和f2()被装饰,f3()没有被装饰。结果如下:
running register(<function f1 at 0x0000018E43507A60>) running register(<function f2 at 0x0000018E43507AE8>) running main() registry -> [<function f1 at 0x0000018E43507A60>, <function f2 at 0x0000018E43507AE8>] running f1() running f2() running f3()
如上可知,register在模块中其他函数之前运行(两次),先于main函数执行。调用register时,传给它的参数是被装饰的函数,例如<function f1 at 0x0000018E43507A60>
加载模块后,registry中有两个被装饰函数的引用:f1和f2。这两个函数,以及f3只有在main函数调用时才执行。
若将示例命名为registration.py然后使用import registration.py导入模块,则出现:
running register(<function f1 at 0x0000018E43507A60>)
running register(<function f2 at 0x0000018E43507AE8>)
以上可知,装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才执行。
变量作用域规则
一段代码:
def f1(a): print(a) print(b) f1(3) #报错
代码报错,原因很简单,b没有赋值。现在先给b赋值:
b = 6 def f1(a): print(a) print(b) f1(3) #结果 3 6
b为一个全局变量,正常输出。再加一点料:
b = 6 def f1(a): print(a) print(b) b = 9 f1(3) #报错
b已经赋值过了,为何上述代码会报错呢。print(a)执行了而print(b)没有执行。事实上,python编译函数定义体时,判断b为局部变量,因为函数中给b赋值了,python从尝试本地环境获取b,调用print(b)时发现b没有绑定值,于是报错。
如果在函数中赋值时想让解释器把b当做全局变量,需要使用global声明:
b = 6 def f1(a): global b print(a) print(b) b = 9 f1(3) #结果 3 6
闭包
学习装饰器,必须了解闭包。
闭包:指的是延伸了作用域的函数,其中包含函数定义体中引用,但是不在定义体中定义的非全局变量。关键:它能访问定义体之外的非全局变量。
定义一个计算平均数的函数,每次新加一个数,得到历史上所有加入的数的平均值。
def make_avg(): series = [] def average(new_value): series.append(new_value) total = sum(series) return total/len(series) return average avg = make_avg() print(avg(10)) print(avg(11)) print(avg(12))
结果:
10.0 10.5 11.0
如上,series是make_avg的局部变量,因为那个函数定义体内初始化了series:serise = [ ]。然而,调用avg(10)时,make_avg函数已经返回了,它本身的作用域也不存在了。
在averager函数中,series是自由变量(在本地作用域中绑定的变量)
上图中,averager函数的闭包延伸到那个函数作用域之外,包含series的绑定。
审查编译后的averager:
print(avg.__code__.co_varnames) #打印局部变量 print(avg.__code__.co_freevars) #打印自由变量 print(avg.__closure__) #__colsure__属性,里面各个元素对应一个自由变量的名称 print(avg.__closure__[0].cell_contents) #取第一个自由变量的值 ('new_value', 'total') ('series',) (<cell at 0x000001CEB8890B58: list object at 0x000001CEC2214788>,) [10, 11, 12]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。
nonlocal声明
每次都要计算所有历史值的总和然后求平均值,显然效率不高,更好的方法是只保留平均值以及个数,然后求平均值。这样写:
def make_avg(): count = 0 total = 0 def average(new_value): count += 1 total += new_value return total/count return average avg = make_avg() print(avg(10))
根据变量域作用规则,count和total不是average函数的局部变量,而直接计算就认为它是局部变量,计算时却又没有绑定值,显然时有问题的。(参见:变量作用域规则)
而上一个average函数也使用了未赋值的series,却没有问题?
事实上,这里利用了列表是可变的对象的这一事实。但是数字,字符串,元组等不可变类型,只能读取,不能更新。若重新绑定,会隐式创建同名局部变量。
python3引入的nonlocal声明解决了这个问题。上述代码改为:
def make_avg(): count = 0 total = 0 def average(new_value): nonlocal count, total count += 1 total += new_value return total/count return average avg = make_avg() print(avg(10))
一个简单装饰器
输出函数运行时间的装饰器:
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('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked #返回内部函数,取代被装饰的函数
使用该装饰器:
@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(1)') snooze(1) print('*' * 40, 'calling factorial(6)') print('6!=', factorial(6))
结果:
**************************************** calling snooze(1) [1.00001869s] snooze(1) -> None **************************************** calling factorial(6) [0.00000073s] factorial(1) -> 1 [0.00001210s] factorial(2) -> 2 [0.00002016s] factorial(3) -> 6 [0.00002896s] factorial(4) -> 24 [0.00003666s] factorial(5) -> 120 [0.00004582s] factorial(6) -> 720 6!= 720
在这个示例中,factorial保存的是clocked函数的引用,每次调用factorial(n),执行的都是clocked(n):
1)记录初始时间
2)调用原来的factorial函数,保存结果
3)计算时间
4)格式化并打印收集的数据
5)返回第2)步保存的结果
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同参数,而且返回被装饰的函数本身该返回的值,同时做一些额外操作。
标准库中的几个装饰器
1.functools.wraps
//保留原函数的属性,保证装饰器不会对被装饰函数造成影响
def deco(func): @functools.wraps(func) def inner(): print('running inner()') return inner #函数deco返回inner对象 @deco #使用deco装饰target def target(): print('running target()') print(target)
不加这个装饰器时:
<function deco.<locals>.inner at 0x00000253D76B8A60>
使用@functools.wraps装饰器之后 ->显示的是原本的函数,保留了原函数__name__,__doc__等属性
<function target at 0x000001B79BD34A60>
2.functools.lru_cache
//缓存数据,避免传入相同的参数时的重复计算
使用递归算法生成第n个斐波那契数:
@clock #使用clock装饰器 def fibonacci(n): if n < 2: return n return fibonacci(n-2) + fibonacci(n-1) if __name__ == '__main__': print(fibonacci(6))
结果:
[0.00000037s] fibonacci(0) -> 0 [0.00000073s] fibonacci(1) -> 1 [0.00004692s] fibonacci(2) -> 1 [0.00000000s] fibonacci(1) -> 1 [0.00000037s] fibonacci(0) -> 0 [0.00000037s] fibonacci(1) -> 1 [0.00001540s] fibonacci(2) -> 1 [0.00003042s] fibonacci(3) -> 2 [0.00009237s] fibonacci(4) -> 3 [0.00000037s] fibonacci(1) -> 1 [0.00000000s] fibonacci(0) -> 0 [0.00000037s] fibonacci(1) -> 1 [0.00001356s] fibonacci(2) -> 1 [0.00002749s] fibonacci(3) -> 2 [0.00000037s] fibonacci(0) -> 0 [0.00000037s] fibonacci(1) -> 1 [0.00001356s] fibonacci(2) -> 1 [0.00000037s] fibonacci(1) -> 1 [0.00000037s] fibonacci(0) -> 0 [0.00000000s] fibonacci(1) -> 1 [0.00001430s] fibonacci(2) -> 1 [0.00002749s] fibonacci(3) -> 2 [0.00005388s] fibonacci(4) -> 3 [0.00009421s] fibonacci(5) -> 5 [0.00020087s] fibonacci(6) -> 8 8
许多重复的计算导致浪费时间,使用lru_cache改善:
@functools.lru_cache() #lru_cache是参数化装饰器,必须加上() 可看下节 参数化装饰器 @clock def fibonacci(n): if n < 2: return n return fibonacci(n-2) + fibonacci(n-1)
时间从0.0002s减少到0.00008s
[0.00000037s] fibonacci(0) -> 0 [0.00000037s] fibonacci(1) -> 1 [0.00005242s] fibonacci(2) -> 1 [0.00000073s] fibonacci(3) -> 2 [0.00006635s] fibonacci(4) -> 3 [0.00000073s] fibonacci(5) -> 5 [0.00008138s] fibonacci(6) -> 8 8
lru_cache使用两个可选参数来配置:lru_cache(maxsize=128,typed=False)
maxsize:缓存个数,满了之后会被扔掉(least recently used 扔掉最近最少使用的数据),理论上应设置为2的幂次
typed:设置为True时,不同类型的参数的运算结果会分开保存,例如1和1.0
3.functools.singledispatch
//类似于c++重载,使用singledispatch装饰的普通函数会变为泛函数:根据第一个参数类型以不同方式执行相同操作的一组函数(称之为单分派;而根据多个参数选择专门的函数,称为多分派)
python不支持重载方法或函数,使用if/elif/elif来处理不同类型的数据显得稍显笨拙,不便于扩展。而functools.singledispatch提供了类似于重载的方式,根据传入的不同类型返回结果
from functools import singledispatch @singledispatch def show(obj): print (obj, type(obj), "obj") @show.register(str) def _(text): print (text, type(text), "str") @show.register(int) def _(n): print (n, type(n), "int") show(1) show("helloworld") show([1])
结果:
1 <class 'int'> int helloworld <class 'str'> str [1] <class 'list'> obj
叠放装饰器
@d1
@d2
def f():
xxx
等同于:
def f():
xxx
f = d1(d2(f))
参数化装饰器
python把被装饰的函数作为第一个参数传给装饰器函数。如果要让装饰器接受其他函数,就需要创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
对于clock装饰器,加一点料,让用户传入一个格式字符串,控制被装饰函数的输出:
import time from functools import wraps DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({arg_str}) -> {_result}' def clock(fmt=DEFAULT_FMT): def decorate(func): @wraps(func) def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args) #获取原函数结果 elapsed = time.perf_counter() - t0 #运行时间 name = func.__name__ #函数名 arg_list = [] if args: arg_list.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_list.append(', '.join(pairs)) arg_str = ', '.join(arg_list) _result = repr(result) print(fmt.format(**locals())) return result return clocked return decorate if __name__ == '__main__': @clock() def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
这个clock装饰器,clock是参数化装饰器工厂函数,decorate是真正的装饰器,clocked包装被装饰的函数;clocked会取代被装饰的函数,返回被装饰的函数原本返回值,decorate返回clocked,clock返回decorete
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
默认输出格式:'[{elapsed:0.8f}s] {name}({arg_str}) -> {_result}' 上述代码输出:
[0.12360011s] snooze(0.123) -> None [0.12296046s] snooze(0.123) -> None [0.12395127s] snooze(0.123) -> None
调整格式:
if __name__ == '__main__': @clock('{name}({arg_str}) dt = {elapsed:0.8f}s') def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)
输出结果
snooze(0.123) dt = 0.12316317s snooze(0.123) dt = 0.12387173s snooze(0.123) dt = 0.12382994s
由于类也是可调用对象,而调用类即调用类的__call__方法,因此类装饰器需要实现__call__方法。事实上,装饰器最好通过实现了__call__方法的类来实现而不是通过普通函数来实现。
以上来自《流畅的python》