[toc]
装饰器是python的一种语法模式,本质上是一种“函数的函数”。装饰器的主要目的是增强函数的功能,当多个函数需要重复使用同一个功能的时候,装饰器可以更加简介的进行实现。装饰器的底层逻辑是 函数的闭包,但这篇文章并不会涉及到。下面的内容将会从一个最简单、常见的案例出发,逐步推导装饰器的实现过程。
函数 是python中最基本的语法单元,假设有这么一个需求:
- 需要统计每个函数的消耗时间
- 需要在函数运行时打印函数名
- 增强函数功能不能影响函数原有的参数和返回值
- 需要对消耗时间做逻辑判断,如超过多少秒就记录下来
如果没有装饰器该如何实现
在没有装饰器的前提下,对一个函数统计其运行时间最简单的办法是直接修改原函数内部代码,比如在开始和结束后分别记录时间戳并进行相减运算。这种模式的劣势在于无法通用,每个函数都得增加重复代码块,且不便于以后的修改。那么很自然的逻辑是把“统计耗时”这个功能的代码块抽象出一个独立的方法,如下:
import time
def timer(func):
start_time = time.time()
func()
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
def func():
print('执行fun 函数...')
time.sleep(1)
timer(func)
用简易装饰器如何实现
这种方法虽然不必要在每个函数进行增加重复的代码,但却需要在函数的调用处修改,将原函数包裹起来。同样,这种方法也不是很好的方式。那么,接下来的方法是,将函数作为参数传入到另一个函数中去,经过添加功能的修饰后,再把修饰后的复合函数返回出来作为一个新的复合函数,这样函数调用的时候,用这个新的复合函数即可。类似于给一颗糖包裹上一层糖衣。如下:
import time
def timer(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
return wrapper
def func():
print('执行fun 函数...')
time.sleep(1)
var = timer(func)
var()
# timer(func)() 上述两行可以等价于这种语法
执行fun 函数...
函数func 消耗时间 1.0023250579833984
这种把函数作为参数的方式传入,经过修饰后再返回的过程就是python装饰器的基本模型。但如上代码所示,这种方式看起来也不是简洁的,每次调用的时候还得使用装饰函数包裹一次,再用新的函数执行。所以在python中这一步骤用@的语法糖进行替代。
import time
def timer(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
return wrapper
@timer
def func():
print('执行fun 函数...')
time.sleep(1)
func()
执行fun 函数...
函数func 消耗时间 1.0023250579833984
如上,函数fun新增的功能通过另外一个装饰性函数实现每次调用核心函数的时候,因为装饰器语法糖的存在,总是先将核心函数作为参数传入装饰函数中,执行修饰形后代码逻辑。相比最初的思路,这种方式简洁、阅读性好,且不需要改动核心函数的逻辑。到此,python装饰器的最简过程已经完成。
如果核心函数有返回值怎么办
上述的模型中,核心函数是一个没有输入和输出的函数。但如果核心函数有输出,即返回值的时候,因为函数生命周期的存在,经过修饰后的复合函数并没有接收到核心函数的返回值。所以如果用上述的最简装饰模型测试,调用函数的返回值为None。
所以,当核心函数有返回值时,必须在装饰函数的内部也将返回值传递出来给复合函数。如下:
import time
def timer(func):
def wrapper():
start_time = time.time()
result = func()
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
return result # 将核心函数的输出值,丢给装饰函数保管
return wrapper
@timer
def func():
print('执行fun 函数...')
time.sleep(1)
return 1024 # 核心函数有输出
result = func()
print(result)
执行fun 函数...
函数func 消耗时间 1.0048329830169678
1024
如果核心函数有参数怎么办
同理,如果核心函数不仅有返回值,也有输入值,即参数,那么修饰函数也必须接受该参数的输入。但在案例中,修饰方法接收的是任意函数,无法确定核心函数的参数类型。所以用万能参数替代 *args, **kwargs
如下:
import time
def timer(func):
def wrapper(*args, **kwargs): # 核心函数携带参数,丢给装饰函数输入
start_time = time.time()
result = func(*args, **kwargs) # 核心函数执行过程接受参数
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
return result # 将核心函数的输出值,丢给装饰函数保管
return wrapper
@timer
def func(x, y):
# 带参数的核心函数
print('执行fun 函数...')
time.sleep(1)
return x+y # 核心函数有输出
result = func(1024, 996)
print(f"核心函数输出:{result}")
print(f"调用函数名称为:{func.__name__}")
执行fun 函数...
函数func 消耗时间 1.0039029121398926
核心函数输出:2020
调用函数名称为:wrapper
在这一过程中,修饰函数并不关心核心函数的具体逻辑,也不关心参数是什么形式。所做的只是把核心函数的参数一起包裹进来,并将核心函数的返回值打包丢给复合函数并丢出来。
关于复合函数的名字
python中函数有自己的名字,通过func.__name__
即可看到。上面的测试代码显示,复合函数的函数名是wrapper
。这样的结果符合代码逻辑,但不符合业务逻辑。例如糖衣包裹的棉花糖应该叫棉花糖,而不是叫糖衣。为解决这个问题,可以在修饰器中返回wrapper
之前,把wrapper
的name重置为func
的name。如下:
# 函数名重置
wrapper.__name__ = function.__name__
wrapper.__doc__ = function.__doc__
return wrapper
作为装饰器的通用功能,python内部用另一个装饰器进行修饰来代替上面这两行代码。functools.wraps(function)
这个内置的装饰器的功能实际上等价于上诉两行代码。
所以上面的装饰器经过再次修饰后,最终的形式如下:
import time
import functools
def timer(func):
@functools.wraps(func) # 内置装饰器,让被修饰后的函数的函数名与原函数一致
def wrapper(*args, **kwargs): # 核心函数携带参数,丢给装饰函数输入
start_time = time.time()
result = func(*args, **kwargs) # 核心函数执行过程接受参数
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
return result # 将核心函数的输出值,丢给装饰函数保管
return wrapper
执行fun 函数...
函数func 消耗时间 1.000427007675171
核心函数输出:2020
调用函数名称为:func
至此,案例中的需求已经基本满足。从头来看完整的过程,即便不考虑底层逻辑,也可以从中发现一个完整的装饰器实现模型:
- 需要给核心函数增加功能
- 将核心函数当参数传入修饰器
- 将核心函数的参数也传入修饰器
- 核心函数的返回值在修饰中被抛出,交给复合函数托管
- 给核心函数添加装饰器语法糖变成一个复合函数
- 调用核心函数实际上是调用复合函数
- 最后解决函数名的归属问题
装饰器的基本模版如下:
如果装饰器需要定制
上述的需求中还有一个对计时进行逻辑判断的需求,如果对于每个核心函数的判断都一样,那么在装饰器中直接增加代码逻辑即可。但如果每个核心函数的判断不一样,或者不同时间的要求不一样,那么就需要定制装饰器,把装饰器再装饰一下,也就是装饰器的套娃。
import time
import functools
def timer_super(max_time=1): # 在原装饰器上继续套娃,增加装饰器参数
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
if (end_time-start_time) > max_time:
print(f"超过最大耗时 {max_time}s")
return result
return wrapper
return timer
@timer_super(0.5)
def func(x, y):
print('执行fun 函数...')
time.sleep(1)
return x+y # 核心函数有输出
result = func(1024, 996)
print(f"核心函数输出:{result}")
print(f"调用函数名称为:{func.__name__}")
执行fun 函数...
函数func 消耗时间 1.001241683959961
超过最大耗时 0.5s
核心函数输出:2020
调用函数名称为:func
总结
装饰器的目的是重构重复且通用代码块,使代码得到简化,提高阅读性。上面的案例中,只是最常用的统计函数运行时间。装饰器还可以应用到打印日志、登陆检查、邮件发送等等具体的业务场景。同时,装饰器的实现也不一定是函数的形式,也可以是装饰器类。但无论是装饰器函数还是装饰器类,其基本逻辑和模版并没有很大的区别。