Python装饰器是实现函数功能增强的常用工具,很多场景下会用来为函数添加计时功能,方便统计函数执行耗时。但当存在嵌套函数调用时,装饰器的计时逻辑可能会被多次触发,导致重复输出计时结果,影响使用体验。

重复计时输出的产生原因
当外层函数和内层函数都被添加了计时装饰器,且外层函数内部调用内层函数时,外层函数的装饰器会触发一次计时,内层函数的装饰器也会触发一次计时,最终就会出现多次计时输出。我们可以通过一个简单的示例来复现这个问题。
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
return result
return wrapper
@timer
def outer_func():
time.sleep(0.5)
inner_func()
time.sleep(0.3)
@timer
def inner_func():
time.sleep(0.4)
outer_func()
运行上述代码后,会输出三次计时结果,分别是inner_func、outer_func的耗时,以及inner_func被outer_func调用时的额外耗时,这就是典型的重复计时问题。
避免重复计时的策略
1. 使用全局标志位控制装饰器生效
我们可以设置一个全局标志位,当装饰器第一次被触发时修改标志位,后续嵌套调用的装饰器检测到标志位已修改就跳过计时逻辑。
import time
# 全局标志位,默认未处于计时流程中
is_timing = False
def timer(func):
def wrapper(*args, **kwargs):
global is_timing
# 如果已经在计时流程中,直接执行函数不输出计时
if is_timing:
return func(*args, **kwargs)
# 开始计时,修改标志位
is_timing = True
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
# 计时结束,恢复标志位
is_timing = False
return result
return wrapper
@timer
def outer_func():
time.sleep(0.5)
inner_func()
time.sleep(0.3)
@timer
def inner_func():
time.sleep(0.4)
outer_func()
这种方式可以解决简单的嵌套调用重复计时问题,但全局标志位在多线程场景下会出现冲突,适用场景有限。
2. 为装饰器添加开关参数
我们可以给装饰器添加参数,允许调用时指定是否启用计时功能,在嵌套场景下手动关闭内层函数的装饰器计时。
import time
def timer(enable=True):
def decorator(func):
def wrapper(*args, **kwargs):
if not enable:
return func(*args, **kwargs)
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
return result
return wrapper
return decorator
@timer(enable=True)
def outer_func():
time.sleep(0.5)
# 调用内层函数时,内层装饰器计时关闭
inner_func()
time.sleep(0.3)
@timer(enable=False)
def inner_func():
time.sleep(0.4)
outer_func()
这种方式灵活性更高,开发者可以根据实际调用场景手动控制每个函数的装饰器是否生效,但需要提前知道函数的调用关系,不够自动化。
3. 使用线程局部变量记录计时状态
针对多线程场景,可以使用线程局部变量来记录当前线程的计时状态,避免全局标志位的冲突问题。
import time
import threading
# 线程局部变量,每个线程独立存储计时状态
local_data = threading.local()
def timer(func):
def wrapper(*args, **kwargs):
# 如果当前线程已经在计时流程中,直接执行函数
if getattr(local_data, 'is_timing', False):
return func(*args, **kwargs)
# 标记当前线程进入计时流程
local_data.is_timing = True
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
# 恢复计时状态
local_data.is_timing = False
return result
return wrapper
@timer
def outer_func():
time.sleep(0.5)
inner_func()
time.sleep(0.3)
@timer
def inner_func():
time.sleep(0.4)
outer_func()
这种方式适配多线程场景,每个线程的计时状态相互独立,不会出现状态冲突的问题。
不同策略的适用场景对比
我们可以通过下表对比三种策略的特点,方便根据实际需求选择:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全局标志位 | 实现简单,代码量少 | 不支持多线程,状态容易冲突 | 单线程简单嵌套调用场景 |
| 装饰器开关参数 | 灵活可控,无状态冲突 | 需要手动配置,不够自动化 | 调用关系明确,可提前配置的场景 |
| 线程局部变量 | 支持多线程,状态独立 | 实现稍复杂,依赖线程局部存储 | 多线程嵌套调用场景 |
总结
Python装饰器在嵌套函数调用中出现重复计时输出的核心原因是装饰器的计时逻辑被多层触发,解决思路是控制装饰器的计时逻辑只在外层函数调用时生效一次。开发者可以根据场景选择全局标志位、装饰器参数、线程局部变量等策略,在保证计时功能正常的同时避免重复输出,提升代码的健壮性和可读性。