python装饰器有着非常重要且广泛的应用。在python语言中,引入装饰器的目的主要是简化代码,避免代码块的重复。装饰器的引入可以对函数进行扩展,使函数具有额外的功能,如事务处理、性能测试、缓存等等。这里我们举「计算函数执行时间」这个例子。

计算某个函数的执行时间,可以使用如下代码:
def func1(): start = time() sleep(1) #do something end = time() print("total
execute time of {} is {}".format(func1.__name__, end - start)) func1()
#Out:total execute time of func1 is 1.002723217010498

可见,让一个函数实现「计算自身执行时间」的功能大致需要三行代码,可能感觉三行还是比较短的,直接放在函数体内部就好了。但是,如果现在有三个函数需要执行这个功能呢?那么代码如下:
def func1(): start = time() sleep(1) end = time() print("total execute time of
{} is {}".format(func1.__name__, end - start)) def func2(): start = time() sleep
(1) end = time() print("total execute time of {} is {}".format(func1.__name__,
end- start)) def func3(): start = time() sleep(1) end = time() print("total
execute time of {} is {}".format(func1.__name__, end - start)) func1() func2()
func3()

上述代码已经出现了很多重复冗余的部分,三个函数「计算执行时间」的代码都是一样的。设想一下,如果我们有1000个函数需要计算时间,那么计算时间部分的代码有3000行,这还是比较恐怖的。所以,我们需要考虑如何能够减少代码量的冗余。

此时就是装饰器大显身手的时候了,装饰器可以对一个函数进行功能上的扩展。装饰器本身也是一个函数,传给他的型参是要被扩展的函数对象,同时也返回一个函数对象。对一个函数用装饰器扩展时,只需要在这个函数定义的时候,使用关键字(语法糖)@声明其装饰器名称。

将函数的「计算执行时间」功能改造成利用装饰器的形式,代码如下:
def timer(func): #这里传进来的func就是被扩展的函数 def wrapper(): start = time() func()
#函数执行的地方,即执行sleep(1) end = time() print("total execute time of {} is {}".format(
func.__name__, end - start)) return wrapper @timer def func1(): sleep(1) @timer
def func2(): sleep(1) @timer def func3(): sleep(1) func1() func2() func3()

根据上述代码可见,原本3行的函数时间计算,变为仅仅1行的@timer,这使得代码变得简洁且代码的可重复利用性变强。假如现在有n个需要调用这个功能的函数,代码量就可以减少2(n-1)行。

值得注意的是,在python中一切事物皆对象,其中函数也是对象。在装饰器的第一行,函数func作为一个函数对象传入装饰器中,这个func后面没有括号,所以可以被传递但不执行;如果函数名后面带有小括号,则该函数会执行。

上述例子都是不需要任何参数的,但有如果不进行任何的参数传递,会导致装饰器总是执行固定的逻辑,仅仅局限在某些单一的场景。我们引入装饰器的目的在于减少代码量,所以当两个逻辑非常相近时,如果用不传参的装饰器,就可能需要写两个装饰器来实现两个相近的逻辑。然而,当装饰器可以传进参数时,完全就可以利用一个装饰器来实现两个或多个相近的逻辑。

例如现在每个函数的休眠时间不再是固定的1s,而是一个变量;现需要对这个sleep的时间做一个判断,判断这个函数是「巨能睡」或是「巨不能睡」。

有两种传递参数的方式:其一是在函数定义的时候传递参数,此时参数的传递需要在语法糖@那一行进行;第二种是在函数执行的时候传递参数,此时参数传递在调用原函数的地方进行。下面是第一种参数传递的代码:
def judger(t): #传入的t是函数定义时传递的参数,语法糖@那一行的 def wrapper(func): if t > 5: print(
"巨能睡") else: print("巨不能睡") func() return wrapper @judger(3) def func1(): sleep(3
) @judger(6) def func2(): sleep(6) #Out: #巨不能睡 #巨能睡

在上述代码中,我们对函数扩展的功能是判定这个函数「能不能睡」,此时就需要「睡眠时间」这个参数传入装饰器。与之前无参数的不一样之处,在于装饰器定义的名称judger后面需要接收语法糖@那一行传进来的参数。在函数定义的时候就讲3和6传入到装饰器中。

第二种是在函数执行时传递参数的方法,为了消除参数个数的限定,一般使用动态参数*args,和**kwargs。其中*args是一个参数的元组(tuple),**kwargs是一个记录关键字及其对应值的字典。其示例代码如下:
def adder(func): #这里传进来的func就是被扩展的函数 def wrapper(*args, **kwargs): total = 0
for item in args: total += item print(total) return wrapper @adder def func1(*
args, **kwargs): pass func1(3, 5, 7) #Out:15
这里我定义的装饰器拓展的功能是,求取函数在执行中的所有参数之和。由于这里没有在函数定义时传参,所以在装饰器第一行没有必要接收@那行传递的参数。

由此可以发现,写一个装饰器最多需要三重嵌套。这种情况对应的是参数传递既有定义函数时传递的参数,也有函数执行时传递的参数。这个三重嵌套也相对简单,只需要在上面第二种情况的基础上,在最外面再嵌套一层,用于接收语法糖@那行传递的参数。这种装饰器大致格式如下:
def judger(*args, **kwargs): #函数定义时传来的参数,即3 def wrapper(func): print(args) def
dec(*args, **kwargs): #函数执行时传递的参数,即2,4,5 """do something""" func(*args, **kwargs
) print(args) return dec return wrapper @judger(3) def func1(*args, **kwargs):
pass func1(2,4,5) #Out: (3,) (2, 4, 5)
这种两个地方都有参数传递的形式算是最复杂的了,也只需要写三个嵌套即可。

最后还有一个小trick,对于上述代码,一般还需要调用一个模块以保留原函数的元信息:
from functools import wraps def judger(*args, **kwargs): @wraps(func) def
wrapper(func): print(args) wraps(func) def dec(*args, **kwargs): """do
something""" func(*args, **kwargs) print(args) return dec return wrapper @judger
(3) def func1(*args, **kwargs): pass func1(2,4,5)

如果不使用这个模块,那么func1会失去他的元信息,如__name__,__doc__等等,导致这些信息都变成了装饰器函数judger中的对应信息。因此,为了让这些属性与我们的直觉更加匹配,最好加上这个模块。

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信