在讲如何写一个装饰器之前,我们首先应该明白两个问题:
- 什么是装饰器?
Python提供了一个特殊的符号:@,这个符号可以理解为一种语法。
@dec
def fun(*args, **kwargs):
pass
以上就是装饰器的基本形式,dec就是一个装饰器,而fun就是被dec装饰的函数。
以上代码段实际上可以转换为如下形式:
def fun(*args, **kwargs):
pass
fun = dec(fun)
在日常开发中,绝大部分会使用第一种使用方式,但第二种方式看起来比较直观,易于理解,它诠释了@这个符号到底做了些什么:将被装饰函数当做参数,传入装饰器,并由装饰器返回一个新的函数。
2. 为什么要使用装饰器?
装饰器是AOP(面向切面编程)的一种实现方式,关于AOP的概念和理解,网上有很多专业的回答,这里我就不赘述了。
举一个例子:
def fun_1():
pass
def fun_2():
pass
def fun_3():
pass
def main():
fun_1()
fun_2()
fun_3()
if __name__ == '__main__':
main()
main函数依次调用fun_1,fun_2,fun_3三个函数,某一天我发现main函数的执行十分耗时,我需要对其调用过程进行监控,以得知是在哪一步耗时最长,造成了性能的瓶颈。
def main():
time_1 = time.time()
fun_1()
time_2 = time.time()
fun_2()
time_3 = time.time()
fun_3()
time_4 = time.time()
print(time_2 - time_1, time_3 - time_2, time_4 - time3)
以上方式很直观,在main函数中将每个调用所花费的时间打印出来即可,但这样做的弊端也很明显。
第一:这种方式直接改变了main函数的函数体,当main函数中调用逻辑非常复杂时,这么做无疑是有风险的。
第二:重复的地方很多,且扩展性很差,试想一下,如果发现了fun_2的调用是性能瓶颈,需要进一步监控fun_2的调用过程(fun_2也调用了很多其他函数来完成自身功能),我就不得不在fun_2中再做一遍这个操作了
基于以上两点,我们可以总结出我们真正想要的,即我们不希望改变原函数,与此同时,对于需要监控调用时间的函数,我们可以很轻易地操作它。那么我么就需要考虑用AOP的思想来进行优化。
AOP(面向切面编程)重点自然是‘切面’二字。就拿上述例子来说,我们希望对fun_1,fun_2,fun_3都监控其调用所花费的时间,目的和纬度都是相同的。
那么我们可以将‘监控函数调用所花费的时间’这个功能,进行切面,专门写一个函数(装饰器)来完成这个功能,然后哪里需要它,我们就将它放到哪里,复用性和扩展性立即有了本质的提高。
import time
def timeit(fun):
"""
这个装饰器用于打印函数调用所花费的时间
"""
def inner(*args, **kwargs):
before = time.time()
res = fun(*args, **kwargs)
after = time.time()
print('invoke %s use time: %s' % (fun.__name__, after - before))
return res
return inner
@timeit
def fun_1():
time.sleep(0.2) # 模拟调用fun_1花费的时间
@timeit
def fun_2():
time.sleep(2) # 模拟调用fun_2花费的时间
@timeit
def fun_3():
time.sleep(0.01) # 模拟调用fun_3花费的时间
def main():
fun_1()
fun_2()
fun_3()
if __name__ == '__main__':
main()
invoke fun_1 use time: 0.200775146484375
invoke fun_2 use time: 2.0006284713745117
invoke fun_3 use time: 0.010970354080200195
毫无疑问的是,使用装饰器的方式来完成我们希望的功能,是非常简便和易用的,做到了一处定义,处处可用,也没有改变原函数的函数体。
如果有小伙伴暂时还看不懂timeit这个函数是如何成为一个装饰器,并装饰其他函数来为该函数增加新功能的,不要着急继续往下看,相信你会豁然开朗的。
要理解装饰器,我们需要回到Python设计哲学中的重要一环:一切皆对象!
何为一切皆对象?
说起对象这个概念,很多小伙伴或许会认为对象即是某个类实例化之后的结果,但在Python中,这么理解就不正确了。
实际上在Python中能看到的,能用到的,都可以称之为对象,不仅是类的实例,也包括int,str,dict等基本数据类型,还有function,method,class,module等等,这些都是对象。
函数的参数不一定非得是什么数字、字符串之类的东西,函数的返回值也不一定是数字、字符串之类的东西,它们可以是任何对象。
既然函数也是对象,那么函数就可以作为参数传递给另一个函数,而另一个函数的返回值又可以是一个新的函数
这么说可能有点绕,没关系,继续往下看
仍然拿以上示例来说,加入我现在还不知道装饰器这个东西,我又只想打印fun_1调用所花费的时间,并且我不想去更改main函数,那该怎么做呢?
so easy,不让我改main函数,那我就去改fun_1函数
def fun_1():
before = time.time()
time.sleep(0.2) # 模拟fun_1调用所花费的时间
after = time.time()
print('invoke %s use time: %s' % ('fun_1', after - before))
实际上这么做,fun_1已经不是原来的那个fun_1了,它已经是一个新的函数,那我何不干脆这么做呢?
def fun_1():
time.sleep(0.2) # 模拟fun_1调用所花费的时间
def time_fun_1():
before = time.time()
fun_1()
after = time.time()
print('invoke %s use time: %s' % ('fun_1', after - before))
fun_1 = time_fun_1 # 将fun_1重新指向time_fun_1这个函数,以后调用fun_1实际是去调用time_fun_1
以上代码应该不难理解,但现在突然又需要打印fun_1和fun_2两个函数的调用花费时间呢,依葫芦画瓢的话,那我们就该这么做:
def fun_1():
time.sleep(0.2) # 模拟fun_1调用所花费的时间
def fun_2():
time.sleep(2) # 模拟fun_2调用所花费的时间
def time_fun_1():
before = time.time()
fun_1()
after = time.time()
print('invoke %s use time: %s' % ('fun_1', after - before))
def time_fun_2():
before = time.time()
fun_2()
after = time.time()
print('invoke %s use time: %s' % ('fun_2', after - before))
fun_1 = time_fun_1 # 将fun_1重新指向time_fun_1这个函数,以后调用fun_1实际是去调用time_fun_1
fun_2 = time_fun_2 # 将fun_2重新指向time_fun_2这个函数,以后调用fun_2实际是去调用time_fun_2
很明显,new_fun_1和new_fun_2四句话有两句半都是重复的,我不想当个复读机,我想优化它。
观察函数体我们可以发现,实际上只有一个东西不一样:fun_1 or fun_2
那就很简单了,只有一个东西不一样,那就干脆把这个东西当成参数不就行了吗?
于是乎,代码就变成了这个样子:
def time_fun(fun): # 参数是一个函数
def inner():
before = time.time()
fun() # 参数fun参与到了inner函数的函数体中
after = time.time()
print('invoke %s use time: %s' % (fun.__name__, after - before))
return inner # 返回值是一个函数
#@time_fun # 和fun_1 = time_fun(fun_1)等价
def fun_1():
time.sleep(0.2) # 模拟fun_1调用所花费的时间
#@time_fun # 和fun_2 = time_fun(fun_2)等价
def fun_2():
time.sleep(2) # 模拟fun_2调用所花费的时间
fun_1 = time_fun(fun_1)
fun_2 = time_fun(fun_2)
考虑到函数返回值和函数参数的问题,那么就该再优化一下下:
def time_fun(fun):
def inner(*args, **kwargs): # inner函数可以接收任何形式的参数
before = time.time()
res = fun(*args, **args) # inner函数将接收到的全部参数,传递给fun,并拿到fun的返回值
after = time.time()
print('invoke %s use time: %s' % (fun.__name__, after - before))
return res # inner函数的返回值实际上就是fun的返回值
return inner
好了,装饰器的基本流程和原理讲到这里就算完了。
不过装饰器还有一些其他的特性和形式,比如用到闭包,比如类装饰器,比如装饰器装饰的是method而不是function,这些东西我在之后的博客中也会讲到。
以上讲述如有谬误,请各位小伙伴留言评论吧。