什么是装饰器
装饰器是一个可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会:
1,处理被装饰的函数,然后把它返回
2,将其替换成另一个函数或者对象
若有个名为decorate的装饰器,则:
@decoratedeftarget():print('running target()')
等价于:
deftarget():print('running target()')
target= decorate(target)
上述两种写法结果一样,函数执行完之后得到的target不一定是原来那个target()函数,而是decorate(target)返回的函数。
确认被装饰的函数会替换成其他函数的一个示例:
defdeco(func):definner():print('running inner()')return inner #函数deco返回inner对象
@deco#使用deco装饰target
deftarget():print('running target()')
target()#调用target,运行inner
print(target) #target时是inner的引用
如下结果,target被替换掉了,它是inner的引用。
running inner().inner at 0x00000253D76B8A60>
严格来说,装饰器只是语法糖。装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。
装饰器两大特性:
1,能把被装饰的函数替换成其他函数(如前所示)
2,加载模块时立即执行
python执行装饰器时机(加载模块时)
registry = [] #保存被装饰的函数的引用
def register(func): #参数是一个函数
print('running register(%s)' % func) #显示被装饰的函数
registry.append(func)return func #返回传入的函数
@registerdeff1():print('running f1()')
@registerdeff2():print('running f2()')deff3():print('running f3()')defmain():print('running main()')print('registry ->', registry)
f1()
f2()
f3()if __name__ == '__main__':
main()
如上,f1()和f2()被装饰,f3()没有被装饰。结果如下:
running register()
running register()
running main()
registry-> [, ]
running f1()
running f2()
running f3()
如上可知,register在模块中其他函数之前运行(两次),先于main函数执行。调用register时,传给它的参数是被装饰的函数,例如
加载模块后,registry中有两个被装饰函数的引用:f1和f2。这两个函数,以及f3只有在main函数调用时才执行。
若将示例命名为registration.py然后使用import registration.py导入模块,则出现:
running register()
running register()
以上可知,装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才执行。
变量作用域规则
一段代码:
deff1(a):print(a)print(b)
f1(3) #报错
代码报错,原因很简单,b没有赋值。现在先给b赋值:
b = 6
deff1(a):print(a)print(b)
f1(3)#结果
3
6
b为一个全局变量,正常输出。再加一点料:
b = 6
deff1(a):print(a)print(b)
b= 9f1(3) #报错
b已经赋值过了,为何上述代码会报错呢。print(a)执行了而print(b)没有执行。事实上,python编译函数定义体时,判断b为局部变量,因为函数中给b赋值了,python从尝试本地环境获取b,调用print(b)时发现b没有绑定值,于是报错。
如果在函数中赋值时想让解释器把b当做全局变量,需要使用global声明:
b = 6
deff1(a):globalbprint(a)print(b)
b= 9f1(3)#结果
3
6
闭包
学习装饰器,必须了解闭包。
闭包:指的是延伸了作用域的函数,其中包含函数定义体中引用,但是不在定义体中定义的非全局变量。关键:它能访问定义体之外的非全局变量。
定义一个计算平均数的函数,每次新加一个数,得到历史上所有加入的数的平均值。
defmake_avg():
series=[]defaverage(new_value):
series.append(new_value)
total=sum(series)return total/len(series)returnaverage
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',)
(,)
[10, 11, 12]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。
nonlocal声明
每次都要计算所有历史值的总和然后求平均值,显然效率不高,更好的方法是只保留平均值以及个数,然后求平均值。这样写:
defmake_avg():
count=0
total=0defaverage(new_value):
count+= 1total+=new_valuereturn total/countreturnaverage
avg=make_avg()print(avg(10))
根据变量域作用规则,count和total不是average函数的局部变量,而直接计算就认为它是局部变量,计算时却又没有绑定值,显然时有问题的。(参见:变量作用域规则)
而上一个average函数也使用了未赋值的series,却没有问题?
事实上,这里利用了列表是可变的对象的这一事实。但是数字,字符串,元组等不可变类型,只能读取,不能更新。若重新绑定,会隐式创建同名局部变量。
python3引入的nonlocal声明解决了这个问题。上述代码改为:
defmake_avg():
count=0
total=0defaverage(new_value):
nonlocal count, total
count+= 1total+=new_valuereturn total/countreturnaverage
avg=make_avg()print(avg(10))
一个简单装饰器
输出函数运行时间的装饰器:
importtimedefclock(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))returnresultreturn clocked #返回内部函数,取代被装饰的函数
使用该装饰器:
@clockdefsnooze(seconds):
time.sleep(seconds)
@clockdeffactorial(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
//保留原函数的属性,保证装饰器不会对被装饰函数造成影响
defdeco(func):
@functools.wraps(func)definner():print('running inner()')return inner #函数deco返回inner对象
@deco#使用deco装饰target
deftarget():print('running target()')print(target)
不加这个装饰器时:
.inner at 0x00000253D76B8A60>
使用@functools.wraps装饰器之后 ->显示的是原本的函数,保留了原函数__name__,__doc__等属性
2.functools.lru_cache
//缓存数据,避免传入相同的参数时的重复计算
使用递归算法生成第n个斐波那契数:
@clock #使用clock装饰器
deffibonacci(n):if n < 2:returnnreturn 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是参数化装饰器,必须加上() 可看下节 参数化装饰器
@clockdeffibonacci(n):if n < 2:returnnreturn 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 importsingledispatch
@singledispatchdefshow(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 int
helloworldstr
[1] obj
叠放装饰器
@d1
@d2
def f():
xxx
等同于:
def f():
xxx
f= d1(d2(f))
参数化装饰器
python把被装饰的函数作为第一个参数传给装饰器函数。如果要让装饰器接受其他函数,就需要创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
对于clock装饰器,加一点料,让用户传入一个格式字符串,控制被装饰函数的输出:
importtimefrom functools importwraps
DEFAULT_FMT= '[{elapsed:0.8f}s] {name}({arg_str}) -> {_result}'
def clock(fmt=DEFAULT_FMT):defdecorate(func):
@wraps(func)def clocked(*args, **kwargs):
t0=time.perf_counter()
result= func(*args) #获取原函数结果
elapsed = time.perf_counter() - t0 #运行时间
name = func.__name__ #函数名
arg_list =[]ifargs:
arg_list.append(','.join(repr(arg) for arg inargs))ifkwargs:
pairs= ['%s=%r' % (k, w) for k, w insorted(kwargs.items())]
arg_list.append(','.join(pairs))
arg_str= ','.join(arg_list)
_result=repr(result)print(fmt.format(**locals()))returnresultreturnclockedreturndecorateif __name__ == '__main__':
@clock()defsnooze(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')defsnooze(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》