一、 闭包
要理解装饰器需要先理解闭包,因为装饰器写法用到了闭包
定义:在一个函数里定义一个函数,内函数是用来外函数的一个变量,外函数的返回值是内函数的函数名(函数对象)
def outer():
a = 10
def inner():
print(a)
return inner
# 要调用inner()函数打印,可以outer()()=inner()
# outer()() 可以拆解为两步,首先执行outer(),返回inner函数名,然后再执行一个括号,相当于inner(),可以用一个变量接收下返回值再给这个变量加括号,即outer()=inner,outer()()=inner()
inner可以引用变量a,因为自己这层找不到,会向外寻找
二、 简单装饰器的实现
定义:装饰器本质上是一个函数,该函数用来处理其他函数,在已有函数功能上,可以不修改已有函数代码实现扩展功能,为它们增加额外的功能,装饰器的返回值也是一个函数对象,简言之,装饰器就是为已经存在的对象添加额外的功能
需求:为所有已有函数增加一个执行时间计算的功能
方案一:直接修改测试用例代码,执行前时间是开始时间,执行后时间是结束时间,相减就是耗时
import time
def foo():
start_time = time.time()
time.sleep(1)
end_time = time.time()
print("运行时间:",end_timestart_time)
foo()
缺点:1、直接修改原代码,修改了已经测试通过没问题的代码,引入了新代码,就可能引入bug,最好是对在不改变原有代码逻辑的基础上扩展代码;2、这个方案所有函数都需要改代码,成本高
方案二:借助高阶函数,定义一个show_time函数,参数传入foo函数名,调用show_time函数
import time
def foo():
time.sleep(1)
def show_time(func):
start_time = time.time()
func()
end_time = time.time()
print("运行时间:",end_time-start_time)
show_time(foo)
优点:上面的方式没有修改foo函数内部逻辑,也没有重写foo函数
缺点:调用的函数名变了,从foo变成了show_time,如果foo函数是上游,下游有依赖会调用foo,那么如果下游如果不跟着改变,下游就会报错,如果之前就有同事会调用foo函数,那么他那就会报错,因为有依赖,他还得改代码,需要将对foo的调用全部改为show_time,成本高
方案三:在方案二的基础上,实现调用show_time()就相当于调用foo(),那么问题就解决了。要实现这个,需要用到闭包,show_time会返回一个函数对象,这个函数对象内是核心业务函数(里面对已有函数功能实现了扩展),由foo来接收,相当于foo指向了核心业务函数,调用foo就是调用核心业务函数,实现了偷梁换柱,没有改变函数调用方式
import time
def foo():
time.sleep(1)
def show_time(func):
def inner():
start_time = time.time()
func()
end_time = time.time()
print("运行时间:",end_time-start_time)
return inner
foo = show_time(foo) # foo = inner
foo()
"""
上述代码执行过程:
1、foo = show_time(foo),调用show_time函数,进入函数内部,第一行是函数定义,不会执行,直接调到return inner,然后foo = inner,实现了偷梁换柱
2、再执行foo(),相当于执行inner(),找到inner函数定义的地方进行执行,计算了时间并返回
"""
foo = show_time(foo),相当于foo = inner,这个时候foo已经指向inner,而inner函数就是对foo函数功能的扩充,是核心业务函数,再调用foo(),就是调用inner(),实现了偷梁换柱
思考:如果show_time里面不写inner函数,直接自己实现逻辑,也就是说show_time内部自己就是和新代码,然后返回show_time行不行?
答案:不行,确实foo = show_time(foo)这句没问题,会得到foo=show_time,然后foo()就相当于执行show_time(),但是会发现,此时调用show_time没有传入参数,执行时会报show_time缺少一个参数,而如果此时foo = inner这种实现方式就没问题,相当于调用inner(),而在inner函数的定义里,没有参数,所以这时候不会报错
三、 语法糖
方案三小缺点:其他函数要使用show_time功能,每次都要进行foo = show_time(foo)这样的赋值,挺麻烦的
解决方案:python提供了一个语法糖功能,在被装饰函数上面@装饰函数,是从上到下的执行逻辑,所以装饰函数需要放在上面
import time
def show_time(func):
def inner():
start_time = time.time()
func()
end_time = time.time()
print("运行时间:",end_time-start_time)
return inner
@show_time
def foo():
time.sleep(1)
foo()
# @show_time这一句就相当于foo = show_time(foo),效果是相同的,也就是调用foo相当于调用inner函数
至此,我们实现了在没有修改已有函数功能的基础上实现了功能扩展,没有修改函数调用方式,且可以利用语法糖方便的进行扩展功能调用
四、结合项目使用进一步完善装饰器功能
先写show_time装饰器,并测试通过
在被装饰函数模块导入show_time,给被装饰函数登录接口增加装饰器@show_time
报错了,inner函数定义时没有参数,但是login传了两个参数(一个是LOGIN_DATA,一个是默认参数getToken=False),所以需要修改装饰器,让inner函数接收参数,传递给func()
然后再次运行被装饰函数
再次修改装饰器
再次执行被装饰函数,成功
跑登录测试脚本代码,可以看到三条登录用例(数据驱动)都计算了接口耗时