python的语法糖——装饰器(上)-爱代码爱编程
对于很多刚刚接触Python的人来说,对装饰器的概念会感到非常模糊,因为几乎没有使用@
作为语法标识的语言,本人唯一能想起来的也只有Java中的注释会以@
来进行标识了。
装饰器是什么
装饰器是一种在不修改原函数的基础上给函数增加额外功能的方法。
在翻看别人的Python源代码的过程中,你肯定见到过一些这样的用法
# 有这样的
@decorator1
def add(x, y):
return x + y
# 也有这样的
@decorator2(arg1, arg2)
def add(x, y):
return x + y
在上面的代码中,两行以@
开头的代码@decorator1
和@decorator2(arg1, arg2)
就是我们今天要介绍的主角——装饰器。
装饰器能做些什么
常见的装饰器功能分为如下几种:
- 记录函数的执行时间
- 记录函数的调用次数
- 缓存函数的结果
- 权限控制
- 类型检查
- …
装饰器的原理是什么
首先我们要明确一个概念,在Python中,万物皆对象,所有东西其实都是object,函数也不例外。
装饰器的本质就是一个函数,它接受一个函数作为参数,并返回一个新的函数。
原函数可以在新函数内部被调用,并且新函数可以在原函数之前或之后添加新的功能。
当使用@decorator
语法将装饰器应用于一个函数时,Python会执行如下操作:
- 首先调用装饰器函数,将原函数作为参数传入
- 装饰器函数返回一个新函数,并将原函数包装在新函数中
- Python 会将原函数的定义替换为新函数的定义
写个简单的装饰器吧
说了这么多的理论,接下来实战一下吧。
有没有这样一种需求,我们在执行代码时,想要知道某函数耗时是多少。通常第一反应想到的是如下实现方法
import time
def my_func():
start = time.time()
# 程序的主逻辑
...
end = time.time()
print(f"function 'my_func' spend {end - start} second.")
my_func()
# function 'my_func' spend x.xxxx seconds.
一个函数还好说,如果我们有多个函数都需要进行计时,岂不是要复制好几份代码了?这可一点也不Pythonic!
如果引入装饰器那就不同了,代码一下子就能变得清晰易读(省的后人骂你)。想对哪个函数计时,直接在这个函数头上加个计时装饰器就可以了。
import time
def timer(func):
def wrapper():
start = time.time()
func()
end = time.time()
print(f"function '{func.__name__}' spend {end - start} seconds.")
return wrapper
@timer # 添加计时用装饰器
def sleep_3():
time.sleep(3)
@timer # 添加计时用装饰器
def sleep_5():
time.sleep(5)
sleep_3()
sleep_5()
# function 'sleep_3' spend 3.0012123584747314 seconds.
# function 'sleep_5' spend 5.0002405643463135 seconds.
我们来分析下这个例子
其实很好理解,其实等于将计时的核心逻辑抽象出来进行了复用。
以sleep_3
函数举例,在添加装饰器后,等价调用形式变为了timer(sleep_3())
。所以装饰器只做了以下三件事:
- 将原有函数逻辑保存在
timer
函数的func
参数中。 - 装饰器通过
return wrapper
,将sleep_3
的功能重定向到了装饰器内部的wrapper
函数。 - 将原有函数逻辑
func
插入在wrapper
函数计时逻辑的中间运行。
看到这里就有人说了,上面的两个例子的函数都没有传参,平时使用函数基本都需要传递参数,但是加上参数这个装饰器就会报错,是不是写的有问题?
别急,我们继续往下看。
写个复杂点的装饰器
其实上面这个装饰器是最简单的版本(阉割版),下面我们来实现更加完整的pro版。
换个需求,这次我们还是需要打印函数运行的耗时,只不过这次的函数会带有参数,并且参数的情况不确定。接下来我们对装饰器进行下改造。
import time
def timer(func):
def wrapper(*args, **kwargs): # 给内部函数
start = time.time()
ret = func(*args, **kwargs)
end = time.time()
print(f"function '{func.__name__}' spend {end - start} seconds.")
return ret
return wrapper
@timer # 添加计时用装饰器
def add(x, y):
return x + y
@timer # 添加计时用装饰器
def square(x):
return x ** 2
print(add(1, 2))
print(square(3))
# function 'add' spend 1.0006356239318848 seconds.
# 3
# function 'square' spend 2.000314474105835 seconds.
# 9
按照惯例,我们来分析下这个例子
其实这个例子只是在上个例子的基础上,对被装饰的函数支持了传参的操作,核心仍是将计时的核心逻辑抽象了出来。
以函数add
举例,在添加装饰器后,等价调用形式变为了timer(add(x, y))
。
而在装饰器内部,发生了以下几件事:
- 将原有函数逻辑保存在
timer
函数的func
参数中。 - 装饰器通过
return wrapper
,将sleep_3
的功能重定向到了装饰器内部的wrapper
函数。 - 由于函数进行了重定向,原有
add
函数中的形参就被传递给了重定向后的函数wrapper
,再将原有函数逻辑func
插入在wrapper
函数计时逻辑的中间运行。
最终经过装饰的函数,也会返回原函数add
的执行结果,也就做到了一个使用无感可传参的装饰器。
看到这里有人就说了,装饰器大部分时间都被定义成了一个全局的命名空间进行使用,也就是定义在文件当中的最外层。但是我们也都知道,并非所有的东西都是适合放到全局的命名空间当中。
是不是你也遇到过这种需求,有很多装饰器想给他们分类和封装到类中,或者希望将其中的某些装饰器抽象到类中。
不知道你是不是也曾经和我一样,尝试翻遍了全网的教程,但最终还是由于各种坑放弃了。
别急关注我,下面一篇你就会知道,原来实现的方式这么简单!