本文始发于我的公众号:TechFlow,原创不易,求个关注程序员
今天是Python专题的第12篇文章,咱们来看看Python装饰器。web
差很少五年前面试的时候,我就领教过它的重要性。那时候我Python刚刚初学乍练,看完了廖雪峰大神的博客,就去面试了。我应聘的并非一个Python的开发岗位,可是JD当中写到了须要熟悉Python。我看网上的面经说到Python常常会问装饰器,我当时想的是装饰器我已经看过了,应该问题不大……面试
没想到面试的时候还真的问到了,面试官问我Python当中的装饰器是什么。因为紧张和遗忘,我支支吾吾了半天也没答上来。我隐约听到了电话那头的一声叹息……编程
时隔多年,我已经不记得那是一家什么公司了(估计规模也不大),但装饰器很重要这个事情给我深深打下了烙印。app
现在若是再有面试官问我Python中的装饰器是什么,我一句话就能给回答了,倒不是我装逼,实际上也的确只须要一句话。Python中的装饰器,本质上就是一个高阶函数。编辑器
你可能不太清楚高阶函数的定义,不要紧,咱们能够类比一下。在数学当中高阶导数,好比二次导数,表示导数的导数。那么这里高阶函数天然就是函数的函数,结合咱们以前介绍过的函数式编程,也就是说是一个返回值是函数的函数。可是这个定义是充分没必要要的,也就是说装饰器是高阶函数,可是高阶函数并不都是装饰器。装饰器是高阶函数一种特殊的用法。函数式编程
在介绍装饰器的具体使用以前,咱们先来了解和熟悉一下Python当中的任意参数。函数
Python当中支持任意参数,它写成*args, **kw。表示的含义是接受任何形式的参数。单元测试
举个例子,好比咱们定义一个函数:测试
def exp(a, b, c='3', d='f'):
print(a, b, c, d)
复制代码
咱们能够这样调用:
args = [1, 3]
dt = {'c': 4, 'd': 5}
exp(*args, **dt)
复制代码
最后输出的结果是1, 3, 4, 5。也就是说咱们用一个list和dict能够表示任何参数。由于Python当中规定必选参数必定写在可选参数的前面,而必选参数是能够不用加上名称标识的,也就是能够不用写a=1,直接传入1便可。那么这些没有名称标识的必选参数就能够用一个list来表示,而可选参数是必需要加上名称标识的,这些参数能够用dict来表示,这二者相加能够表示任何形式的参数。
注意咱们传入list和dict的时候前面加上了*和**,它表示将list和dict当中的全部值展开。若是不加的话,list和dict会被当成是总体传入。
因此若是一个函数写成这样,它表示能够接受任何形式的参数。
def exp(*args, **kw):
pass
复制代码
明白了任意参数的写法以后,装饰器就不难了。
既然咱们能够用*args, **kw接受任何参数。而且Python当中支持一个函数做为参数传入另一个函数,若是咱们把函数和这个函数的全部参数所有传入另一个函数,那么不就能够实现代理了吗?
仍是刚才的例子,咱们额外增长一个函数:
def exp(a, b, c='3', d='f'):
print(a, b, c, d)
def agent(func, *args, **kwargs):
func(*args, **kwargs)
args = [1]
dt = {'b': 1, 'c': 4, 'd': 5}
agent(exp, *args, **dt)
复制代码
装饰器的本质其实就是这样一个agent函数,可是若是使用的时候须要手动传入会很是麻烦,使用起来不太方便。因此Python当中提供了特定的库,咱们可让装饰器以注解的方式使用,大大简化操做:
from functools import wraps
def wrapexp(func):
def wrapper(*args, **kwargs):
print('this is a wrapper')
func(*args, **kwargs)
return wrapper
@wrapexp
def exp(a, b, c='3', d='f'):
print(a, b, c, d)
args = [1, 3]
dt = {'c': 4, 'd': 5}
exp(*args, **dt)
复制代码
在这个例子当中,咱们定义了一个wrapexp的装饰器。咱们在其中的wrapper方法当中实现了装饰器的逻辑,wrapexp当中传入的参数func是一个函数,wrapper当中的参数则是func的参数。因此咱们在wrapper当中调用func(*args, **kw),就是调用打上了这个注解的函数自己。好比在这个例子当中,咱们没有作任何事情,只是在原样调用以前多输出了一行’this is a wrapper',表示咱们的装饰器调用成功了。
咱们理解了装饰器的基本使用方法以后,天然而然地会问一个自然的问题,学会了它究竟有什么用呢?
若是你从上面的例子当中没有领会到装饰器的强大,不如让我用一个例子再来暗示一下。好比说你是一个程序员,辛辛苦苦作出了一个功能,写了好几千行代码,上百个函数,终于经过了审核上线了。这个时候,你的产品经理找到了你说,通过分析咱们发现上线的功能运行速度不达标,常常有请求超时,你能不能计算一下每一个函数运行的耗时,方便咱们找到须要优化的地方?
这是一个很是合理的请求,但想一想看你写了上百个函数,若是每个函数都要手动添加时间计算,这要写多少代码?万一哪一个函数不当心改错了,你又得一一检查,而且若是要求严格的话你还得为每个函数专门写一个单元测试……
我想,正常的程序员应该都会抗拒这个需求。
可是有了装饰器就很简单了,咱们能够实现一个计算函数耗时的装饰器,而后咱们只须要给每个函数加上注解就行了。
import time
from functools import wraps
def timethis(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
复制代码
这也是装饰器最大的用途,能够在不修改函数内部代码的前提下,为它包装一些额外的功能。
咱们以前说过装饰器的本质是高阶函数,因此咱们也能够和高阶函数同样来调用装饰器,好比下面这样:
def exp(a, b, c='3', d='f'):
print(a, b, c, d)
args = [1, 3]
dt = {'c': 4, 'd': 5}
f = wrapexp(exp)
f(*args, **dt)
复制代码
这样的方式获得的结果和使用注解是同样的,也就是说咱们加上注解的本质其实就是调用装饰器返回一个新的函数。
既然和高阶函数是同样的,那么就带来了一个问题,咱们使用的其实已经再也不是原函数了,而是一个由装饰器返回的新函数,虽然这个函数的功能和原函数同样,可是一些基础的信息其实已经丢失了。
好比咱们能够打印出函数的name来作个实验:
正常的函数调用__name__返回的都是函数的名称,可是当咱们加上了装饰器的注解以后,就会发生变化,一样,咱们输出加上了装饰器注解以后的结果:
咱们会发现输出的结果变成了wrapper,这是由于咱们实现的装饰器内部的函数叫作wrapper。不只仅是__name__,函数内部还有不少其余的基本信息,好比记录函数内描述的__doc__,__annotations__等等,这些基本信息被称为是元信息,这些元信息因为咱们使用注解发生了丢失。
有没有什么办法能够保留这些函数的元信息呢?
其实很简单,Python当中为咱们提供了一个专门的装饰器用来保留函数的元信息,咱们只须要在实现装饰器的wrapper函数当中加上一个注解wraps便可。
def wrapexp(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('this is a wrapper')
func(*args, **kwargs)
return wrapper
复制代码
加上了这个注解以后,咱们再来检查函数的元信息,会发现它和咱们预期一致了。
了解了Python中的装饰器以后,再来看以前咱们用过的@property, @staticmethod等注解,想必都能明白,它们背后的实现其实也是装饰器。灵活使用装饰器能够大大简化咱们的代码,让咱们的代码更加规范简洁,还能灵活地实现一些特殊的功能。
装饰器的用法不少,今天介绍的只是其中最基本的,在后续的文章当中,还会继续和你们分享它更多其余的用法。在文章开始的时候我也说了,装饰器是Python进阶必学的技能之一。想要熟练掌握这门语言,灵活运用,看懂大佬的源码,装饰器是必须会的东西。
但愿你们都能有所收获,原创不易,厚颜求个赞和关注~