python装饰器
1、什么是装饰器?
装饰器实际上就是在不用更改原函数代码的前提下给函数增加新的功能
。因为程序已经上线或被使用,那么就不能大批量的修改源代码,这样是不科学的也是不现实的,因此就产生了装饰器,使得其满足:
- (1)不能修改被装饰的函数的源代码
- (2)不能修改被装饰的函数的调用方式
- (3)满足1、2的情况下给程序增添功能
装饰器的组成:
装饰器 = 函数 + 实参高阶函数 + 返回值高阶函数 + 嵌套函数 + 语法糖
2、实现装饰器
对于下面这个简单的函数:
import time
def test():
print("happy")
time.sleep(1)
print("a day...")
if __name__ == '__main__':
f = test
f()
# 输出
"""
happy
a day...
"""
如果我们试图去记录这个函数执行的总时间,但不能直接去改我们的核心代码,即func函数,应该如何去做?
2.1 函数变量
考虑到函数同变量一样,都是“一个名字对应内存地址中的一些内容” ,对于test和test():
- test表示的是函数的
内存地址
,赋值给f,相当于f也指向这块地址; - test()就是调用在test这个
地址的内容
,即函数。
接下来考虑将目的函数作为变量传给另一个函数,从而来调用该函数,并且加入一些功能,那么我们在上面代码中引入高阶函数:
2.2 高阶函数
高阶函数的形式可以有两种:
把一个函数名当作实参传给另外一个函数(“实参高阶函数
”);
返回值中包含函数名(“返回值高阶函数
”)。
下面使用实参高阶函数:
import time
def deco(func):
start_time = time.perf_counter()
func()
end_time = time.perf_counter()
mses = (end_time - start_time) * 1000
print("time is %d ms" % mses)
def test():
print("happy")
time.sleep(1)
print("a day...")
if __name__ == '__main__':
f = test
deco(f)
# 输出
"""
happy
a day...
time is 1000 ms
"""
由上面代码,我们实现了需求,满足了不修改程序源码和为程序添加功能两个条件,但是更改了函数的调用方式,不满足条件(2)。
下面使用返回值高阶函数:
def deco(func):
print(func)
return func
def test():
print("happy")
time.sleep(1)
print("a day...")
if __name__ == '__main__':
test = deco(test)
test()
# 输出
"""
<function test at 0x0000024023E12268>
happy
a day...
"""
上面的代码函数调用方式为:test => func => test,将test作为返回值传回去,虽然没有修改调用方式,也加入了一些东西,但是无法实现计时的功能,于是我们考虑再加入嵌套函数。
2.3 嵌套函数
嵌套函数指的是在函数内部定义一个函数,而不是调用,如:
def func1():
def func2():
pass
# 而不是
def func1():
func2()
注:函数只能调用和它同级别以及上级的变量或函数。也就是说:内层的函数能调用和它缩进一样的和他外部的,而内部的是无法调用的。
上面程序中引入嵌套函数:
# 引入嵌套函数:
def deco(func):
def wrapper():
start_time = time.perf_counter()
func()
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("time is %d ms" % mses)
return wrapper
def test():
print("happy")
time.sleep(1)
print("a day...")
if __name__ == '__main__':
test = deco(test)
test()
print("test's name is: ", test.__name__)
# 输出:
"""
happy
a day...
time is 1000 ms
test's name is: wrapper
"""
上面函数的调用过程是:test -> deco -> wrapper -> test,实际执行时test指向的是wrapper这个函数,wrapper函数内部调用了test,并且内部添加了计时功能。
通俗的理解:把函数看成是盒子,test是小盒子,wrapper是中盒子,deco是大盒子。程序中,把小盒子test传递到大盒子deco中的中盒子wrapper,然后再把中盒子wrapper拿出来(返回),打开看看(调用)。
到这里基本上完成了我们所要的功能,并且满足了装饰器的三个条件,但是对于需要装饰的每个函数都要执行test = deco(test)
,未免有些麻烦,也不太美观,于是继续引入python的语法糖。
2.4 语法糖
Python提供了一种语法糖,即:
@timer
# 等价于
test = deco(test)
这两句是等价的,只要在函数前加上这句,就可以实现装饰的作用。
因此,一个基本装饰器的最终实现:
import time
def deco(func):
def wrapper():
start_time = time.perf_counter()
func()
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("time is %d ms" % mses)
return wrapper
@deco # 等价于func = deco(func)
def test():
print("happy")
time.sleep(1)
print("a day...")
if __name__ == '__main__':
test()
print("test's name is: ", test.__name__)
# 输出
"""
happy
a day...
time is 1000 ms
test's name is: wrapper
"""
这里的deco函数就是最原始的装饰器,它的参数是一个函数,然后返回值也是一个函数。其中,作为参数的这个函数test()就在返回函数wrapper()的内部执行。然后在函数test()前面加上@deco,test()函数就相当于被注入了计时功能,现在只要调用func(),它就已经变身为“新的功能更多”的函数了(实际调用的是wrapper)。
以上为无参形式。
对于一个实际问题,函数往往是有参数的,那么我们应该如何处理?下面引入参数:
3、装饰有参函数
def deco(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("time is %d ms" % mses)
return wrapper
@deco
def test(parameter):
print("%s" % parameter, end=" ")
time.sleep(1)
print("day...")
if __name__ == '__main__':
test("cold")
print("test's name is: ", test.__name__)
# 输出:
"""
cold day...
time is 999 ms
test's name is: wrapper
"""
由于test是指向wrapper的,所以wrapper要引入参数,而wrapper内部调用test也需要参数,因此内部test也要引入参数。所以,就必须给wrapper()和test()都加上参数,为了使程序更加有扩展性,因此在装饰器中的deco()和test(),加入可变参数*agrs和 **kwargs。
引入参数后,完整实现如上所示。
那么我们再考虑个问题,如果原函数test()的结果有返回值呢?比如:
def test(parameter):
time.sleep(1)
print("happy day...")
return "return some value"
那么面对这样的函数,如果用上面的代码来装饰,最后的test()实际上调用的是wrapper()。有人可能会问,func()不就是test()么,怎么没返回值呢?
其实是有返回值的,但是返回值返回到wrapper()的内部,test()的返回值实际是wrapper,那么就需要保存func()内部的返回值,然后再返回,因此就是:
def deco(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("time is %d ms" % mses)
return res
return wrapper
@deco
def test(parameter):
print("%s" % parameter, end=" ")
time.sleep(1)
print("day...")
return "return some value"
if __name__ == '__main__':
result = test("cold")
print("返回值:", result)
print("test's name is: ", test.__name__)
# 输出:
"""
cold day...
time is 1000 ms
返回值: return some value
test's name is: wrapper
"""
以上实现了有参函数的装饰器,但还差最后一步。因为函数也是对象,它有__name__等属性,但是看以下经过deco装饰之后的函数,如上输出信息,它们的__name__已经从原来的’test’变成了’wrapper’。
所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。
4、修改装饰后的函数属性
不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的deco的写法如下:
import functools
def deco(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("time is %d ms" % mses)
return res
return wrapper
# 输出:
"""
test's name is: test
"""
import functools是导入functools模块,在定义wrapper()的前面加上@functools.wraps(func)即可。
5、带参数的装饰器
又增加了一个需求,一个装饰器,对不同的函数有不同的装饰。那么就需要知道对哪个函数采取哪种装饰。因此,就需要装饰器带一个参数来标记一下。例如:
@decorator(parameter = value)
比如有两个函数:
def task1():
time.sleep(2)
print("in the task1")
def task2():
time.sleep(2)
print("in the task2")
task1()
task2()
要对这两个函数分别统计运行时间,但是要求统计之后输出:
the task1/task2 run time is : 2.00……
于是就要构造一个装饰器deco,并且需要告诉装饰器哪个是task1,哪个是task2,也就是要这样:
@deco(parameter='task1')
def task1():
time.sleep(1)
print("in the task1")
@deco(parameter='task2')
def task2():
time.sleep(1)
print("in the task2")
task1()
task2()
那么方法有了,我们需要考虑如何把这个parameter参数传递到装饰器中,我们以往的装饰器,都是传递函数名字进去,而这次,多了一个参数,要怎么做呢?
于是,就想到再加一层函数来接受参数,根据嵌套函数的概念,要想执行内函数,就要先执行外函数,才能调用到内函数,那么最终得到:
def deco(parameter):
def outer_wrapper(func):
def wrapper(*args, **kwargs):
if parameter == 'task1':
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("the task1 run time is: ", mses)
return res
elif parameter == 'task2':
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time) * 1000
print("the task2 run time is: ", mses)
return res
return wrapper
return outer_wrapper
@deco(parameter='task1')
def task1():
time.sleep(1)
print("in the task1:")
@deco(parameter='task2')
def task2():
time.sleep(1)
print("in the task2:")
if __name__ == '__main__':
task1()
task2()
# 输出:
"""
in the task1:
the task1 run time is: 999.8678100000001
in the task2:
the task2 run time is: 1000.6716250000001
"""
6、多个装饰器
如果一个函数需要加入很多功能,一个装饰器怕是搞不定,此时可以使用多个装饰器。
import time
import functools
def deco1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器1:调用test前")
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("deco1 time is %d ms" % mses)
print("装饰器1:调用test,计算时间后")
return res
return wrapper
def deco2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器2:调用test前")
start_time = time.perf_counter()
res = func(*args, **kwargs)
end_time = time.perf_counter()
mses = (end_time - start_time)*1000
print("deco2 time is %d ms" % mses)
print("装饰器2:调用test,计算时间后")
return res
return wrapper
@deco1
@deco2
def test(parameter):
print("%s" % parameter, end=" ")
time.sleep(1)
print("day...")
return "return some value"
if __name__ == '__main__':
result = test("cold")
print("返回值:", result)
print("test's name is: ", test.__name__)
输出:
装饰器1:调用test前
装饰器2:调用test前
cold day...
deco2 time is 1000 ms
装饰器2:调用test,计算时间后
deco1 time is 1000 ms
装饰器1:调用test,计算时间后
返回值: return some value
test's name is: test
多个装饰器执行的顺序由上面的输出结果可以知道。
参考: