一、什么是装饰器
装饰器顾名思义就是装饰的工具,指的是在不改变被装饰对象的源代码以及调用方式的前提下,为被装饰对象增加额外的功能,装饰器经常使用在以下几个场景,比如:插入日志、性能测试、事务处理、缓存、权限校验、测试等应用场景
二、为何要有装饰器
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。我们对现有的代码需要扩展功能时不能修改源代码以及调用方式,否则稍有不慎,就会导致整个程序报错,因为我们要修改的这段程序可能在很多地方调用,所以修改源代码和调用方式是很不明智的。而装饰器则可以在不修改源代码和调用方式的情况为原程序拓展新的功能。
三、装饰器的实现思路
现在有一段程序,用来计算0~n的和
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
res = add(100000)
print(res)
输出:
>> 4999950000
如果想计算一下这个函数的运行时间,在修改源代码的情况下我们是这样做的
import time
def add(n):
start = time.time()
sum = 0
for i in range(n):
sum += i
stop = time.time()
print('function run time: %s'%(stop - start))
return sum
res = add(100000)
print(res)
输出:
>> function run time: 0.014251947402954102
>> 4999950000
上面的代码虽然实现了为函数体扩展计算函数运行时间的功能,但是违背了我们开头所说的开放封闭原则,为函数增加新功能是不能修改源代码的!那么在不修改源代码的情况下,如何为其加新功能呢?我们可以在调用的时候,计算函数体运行的时间
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
start = time.time()
res = add(100000)
print(res)
stop = time.time()
print('function run time: %s'%(stop - start))
输出:
>> 4999950000
>> function run time: 0.014489889144897461
现在实现了在不修改源代码以及调用方式的情况下,为原函数增加了计算运行时间的功能,既没有违背开放封闭原则,也扩展了新功能,看似是完美,但是有没有想过,我们可能在很多地方都调用了add函数,那我们要在每次调用的时候都写这么一堆代码?这完全是重复且冗余的代码,说到重复冗余的代码,有些同学就想到了,我们可以封装成一个函数啊!啧啧~
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
def wrapper():
start = time.time()
res = add(100000)
print(res)
stop = time.time()
print('function run time: %s'%(stop - start))
res = wrapper()
print(res)
输出:
>> 4999950000
>> function run time: 0.015117883682250977
>> None
好了,现在我们将拓展的功能封装成一个函数了,这样我们就省去了一大堆的重复冗余代码,但是大家有没有察觉到,上面的代码是有问题的
第一个问题: add 函数是有返回值的,但是我们的新函数wrapper(这里可以称之为装饰器了)返回值是None。
第二个问题 :调用 add 函数是需要传一个参数进去的,而装饰器函数却没有传参,这不就违背了文章开头提到的开放封闭原则了么
是的 所以我们还可以将函数优化一下
解决第一个问题:在装饰器函数里面要把调用add函数返回的值给return出来
解决第二个问题:在装饰器函数里面,把调用add函数时传的值参数化
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
def wrapper(n):
start = time.time()
res = add(n)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
res = wrapper(10000)
print(res)
输出:
>> function run time: 0.0009837150573730469
>> 49995000
至此,我们已经在不违背开放封闭原则的前提下为 add 函数拓展了计算运行时间的功能,这样就大功告成了么? 不不不,我们现在只是实现了为 add 函数扩展了计算运行时间的功能而已,如果我们还需要为其他函数增加计算运行时间的功能怎么办,难道要再写一个吗?装饰器是通用的,所以我们要考虑一下,函数名能不能参数化?(函数对象的概念), 我们先尝试一下把函数名参数化,看看会有什么问题~
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
def index(x,y):
print('from index %s:%s'%(x,y))
def wrapper(func,n):
start = time.time()
res = func(n)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
res_add = wrapper(add,10000)
print(res_add)
wrapper(index,1,2)
输出:
>> function run time: 0.0007648468017578125
>> 49995000
File "/Users/qts/PycharmProjects/QTS/test/ll.py", line 45, in <module>
res_index = wrapper(index,1,2)
TypeError: wrapper() takes 2 positional arguments but 3 were given
将函数名参数化以后,新增index函数进行测试一下,调用wrapper为index函数增加扩展功能,出现了问题,什么问题呢?我们来看一下
第一个问题:index 函数是需要传两个参数的,但是装饰器函数wrapper 只有一个位置形参可以传值,另一个参数怎么传?
首先解决第一个问题,被装饰的对象有几个参数我们不知道,那么我们可不可以用不定长参数来接收呢,这里就用到args,**kwags来接收传进来的参数打包成一个元组或者字典,然后在函数体内使用args,**kwargs 把元组或者字典重新打散传值给func,这样就实现了我们给wrapper传什么参数,func接收的就是什么参数
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
def index(x,y):
print('from index %s:%s'%(x,y))
def wrapper(func,*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
res_add = wrapper(add,10000)
print(res_add)
wrapper(index,1,2)
输出:
>> function run time: 0.0012209415435791016
>> 49995000
>> from index 1:2
>> function run time: 1.0013580322265625e-05
第二个问题:我们在调用的时候多传了一个形参func,因为我们需要通过func 传入被装饰的函数对象,装饰器函数改动了形参,改变了原函数的调用方式,违背了开头说的开放封闭原则
解决第二个问题就使用到了闭包函数的概念,不了解的小伙伴可以去百度一下,闭包函数其实就是第二种为函数体传参的方式,通过闭包函数的方式我们解决了第二个问题,现在我们就已经实现了完整的无参装饰器了。
import time
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
def index(x,y):
print('from index %s:%s'%(x,y))
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
return wrapper
add = outter(add)
res = add(10000)
print(add)
index = outter(index)
index(2,1)
输出:
>> function run time: 0.0017659664154052734
>> 49995000
>> from index 2:1
>> function run time: 1.621246337890625e-05
另外提一点命名空间的概念,调用outter函数返回的是wrapper的函数对象,我们在全局作用域拿到后可以赋值给x,可以赋值给y,也可以赋值给原函数名,把原函数名指向函数对象给覆盖,我们就像是在调用原函数名一样调用装饰器了,是不是很完美~,其实python就已经帮我们实现这件事了,这里要介绍一个令人快乐的东西叫语法糖
语法糖
在被装饰对象上面加上@装饰器,就相当于 add = outter(add) 然后我们调用add时,其实就是调用的outter函数返回的wrapper
import time
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
return wrapper
@outter
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
@outter
def index(x,y):
print('from index %s:%s'%(x,y))
res = add(10000)
print(res)
index(1,2)
输出:
>> function run time: 0.001477956771850586
>> 49995000
>> from index 1:2
>> function run time: 3.314018249511719e-05
再多说一句哈,现在我们实现的装饰器是不是天衣无缝了?我们在调用的时候完全不用关心原函数扩展了啥功能,而且也不用改变原来的调用方式,但是还是有一点区别的,比如函数自带的一些方法 name、doc 还是有区别的,我们既然要伪装了,就一定要力争完美,这里可以使用 functools 的 wraps 方法
import time
from functools import wraps
def outter(func):
@wraps(func)
def wrapper(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
stop = time.time()
print('function run time: %s'%(stop - start))
return res
return wrapper
@outter
def add(n):
sum = 0
for i in range(n):
sum += i
return sum
@outter
def index(x,y):
print('from index %s:%s'%(x,y))
无参装饰器讲完了,后续有机会讲一下有参装饰器以及叠加装饰器的用法~