Python计时装饰器案例:如何精准统计函数耗时(附完整代码)
📚 目录导读
- 为什么要用计时装饰器?
- 装饰器基础回顾:闭包与函数嵌套
- 案例1:最简计时装饰器(入门版)
- 案例2:支持参数与返回值的通用计时器
- 案例3:带日志输出与累计统计的高级计时器
- 案例4:使用time.perf_counter提升精度
- 性能对比:装饰器vs手动计时
- 常见问题解答(Q&A)
为什么要用计时装饰器?
在开发Python应用,尤其是后端服务、数据分析和算法优化时,统计函数耗时是性能调优的核心环节,手动在每个函数开头结尾插入time.time()不仅冗余,还会让代码结构混乱,装饰器(Decorator)恰好能解决这个问题——它允许你在不修改原函数代码的情况下,为其添加额外的功能(如计时、日志、缓存)。

核心优势:一次定义,处处复用;代码解耦,可读性高。
装饰器基础回顾:闭包与函数嵌套
Python装饰器本质是一个接受函数作为参数并返回新函数的闭包,其基本骨架如下:
def my_decorator(func):
def wrapper(*args, **kwargs):
# 在调用前执行的操作
result = func(*args, **kwargs)
# 在调用后执行的操作
return result
return wrapper
计时装饰器就是在wrapper中获取函数执行前后的时间差。
案例1:最简计时装饰器(入门版)
import time
def timer_v1(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_v1
def slow_add(a, b):
time.sleep(1)
return a + b
print(slow_add(3, 5)) # 输出:slow_add 耗时:1.0012 秒,然后输出 8
问题:此版本仅打印耗时,无法保存统计信息;且未处理print重定向场景。
案例2:支持参数与返回值的通用计时器
实际业务中,函数可能有不同的参数数量和返回值,通过*args, **kwargs可以完美适配:
def timer_v2(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[{func.__name__}] args={args}, kwargs={kwargs} | 耗时:{elapsed:.3f}s")
return result
return wrapper
@timer_v2
def fetch_api(user_id, retries=3):
import random
time.sleep(random.uniform(0.5, 1.5))
return {"user_id": user_id, "status": "ok"}
fetch_api(1001, retries=5)
# 输出:[fetch_api] args=(1001,), kwargs={'retries': 5} | 耗时:1.234s
优势:实时打印参数,便于调试,但大量调用时会输出过多日志,影响性能。
案例3:带日志输出与累计统计的高级计时器
在多次调用的场景(如测试循环或HTTP请求),你需要累计总耗时、平均耗时、调用次数等信息:
import time
from functools import wraps
def timer_v3(log_to=None):
"""
高级计时器:支持日志输出和控制台返回
:param log_to: str, 若指定则为文件路径;None则打印到控制台
"""
def decorator(func):
func._call_count = 0
func._total_time = 0.0
@wraps(func) # 保留原函数的元信息(如__name__, __doc__)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# 更新统计
func._call_count += 1
func._total_time += elapsed
# 输出信息
info = (
f"[{func.__name__}] 第{func._call_count}次调用 "
f"耗时:{elapsed:.4f}s | "
f"累计耗时:{func._total_time:.4f}s | "
f"平均耗时:{func._total_time / func._call_count:.4f}s"
)
if log_to:
with open(log_to, "a", encoding="utf-8") as f:
f.write(info + "\n")
else:
print(info)
return result
return wrapper
return decorator
@timer_v3(log_to="./perf.log")
def heavy_compute(n):
total = 0
for i in range(n * 1000000):
total += i
return total
for i in range(3):
heavy_compute(i + 1)
运行后:控制台实时显示统计,且perf.log文件会持久化记录所有调用详情。
案例4:使用time.perf_counter提升精度
time.time()受系统时钟调整影响(如NTP同步),在高精度场景推荐使用time.perf_counter(),它提供纳秒级精度且不受系统时间影响:
import time
def timer_precision(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 耗时:{elapsed * 1e6:.2f} 微秒") # 转为微秒
return result
return wrapper
@timer_precision
def fast_op():
return sum(range(1000))
fast_op() # 输出:fast_op 耗时:约15.23 微秒
适用场景:微秒级函数(如纯数学计算、小型IO操作)。
性能对比:装饰器vs手动计时
| 方式 | 代码耦合度 | 复用性 | 可维护性 | 精度损失 |
|---|---|---|---|---|
| 手动插入time | 高(污染原函数) | 差 | 低 | 极小 |
| 装饰器 | 低 | 极好 | 高 | 01微秒内(可忽略) |
实际建议:装饰器带来的额外开销(函数嵌套调用)大约在 1~0.5微秒,对于耗时>1毫秒的函数完全可以忽略。
常见问题解答(Q&A)
Q1:为什么我的装饰器没有生效?
A:最常见原因是未正确添加语法或装饰器定义顺序错误,确保装饰器函数定义在被装饰函数之前,且@decorator_name紧贴函数定义。
Q2:如何让装饰器同时支持函数和类方法?
A:使用functools.wraps处理元信息,并且wrapper接收self参数,示例:
def method_timer(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
start = time.perf_counter()
result = func(self, *args, **kwargs)
print(f"{func.__name__} 耗时:{time.perf_counter() - start:.4f}s")
return result
return wrapper
Q3:计时装饰器会影响性能吗?
A:单次调用额外开销约0.1~1微秒(取决于time.perf_counter的调用频率),如果函数本身耗时极短(<1微秒),建议仅用于统计采样,而不是每次调用都计时。
Q4:有没有更专业的性能分析工具?
A:有,Python内置cProfile模块可分析函数调用次数与耗时;第三方库line_profiler能逐行统计,但装饰器作为轻量级、细粒度的计时方案,仍是日常开发首选。
Q5:如何保留原函数的文档字符串?
A:务必使用@wraps(func),它将原函数的__name__、__doc__、__module__等属性复制到wrapper函数,否则会导致help(func)或IDE提示失真。
本文通过4个递进案例,展示了Python计时装饰器的完整实现路径:
- 入门版:最简计时
- 通用版:支持任意参数
- 统计版:累计耗时与日志输出
- 高精度版:微秒级统计
最佳实践:优先使用time.perf_counter;生产环境建议配合日志框架(如logging)记录耗时;对高频调用函数,可增加采样率控制(如每100次记录一次)。
你可以轻松地将这些计时装饰器集成到已有项目中,快速定位性能瓶颈了,动手试试吧!