性能优化的第一步永远是找到瓶颈在哪里,而不是凭感觉去改。Python 提供了两种常用的 CPU 耗时分析工具:cProfile 适合整体概览,line_profiler 适合逐行细查。
cProfile:整体性能画像
cProfile 是标准库自带的性能分析器,统计每个函数的调用次数、总耗时、平均耗时等。开销较低,适合快速定位“哪个函数最慢”。
基本用法:
import cProfile
import re
def slow_function():
return re.compile(r"a*b*c*").match("a" * 1000)
cProfile.run("slow_function()")
运行后会输出类似这样的表格:
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.001 0.001 0.001 0.001 script.py:4(slow_function)
- ncalls:调用次数
- tottime:函数自身耗时(不含内部调用的子函数)
- cumtime:函数总耗时(含内部子函数)
- 排序后很快能看出哪个函数占用了大部分 CPU 时间。
更实用的方式:
- 用
-o保存分析结果到文件,再用pstats模块交互式查看。
python -m cProfile -o output.prof my_script.py
然后在代码里或命令行中用 pstats 排序、过滤。
适用场景:首次对程序进行性能体检,快速定位高耗时的函数或模块。
line_profiler:逐行性能剖析
cProfile 只能告诉你哪个函数慢,但进到函数内部,到底是循环太多、还是某条语句特别耗时,它就鞭长莫及了。这时需要 line_profiler。
line_profiler 是第三方库(需 pip install line_profiler),可以给指定函数加上 @profile 装饰器,执行后显示每一行的运行次数、每次耗时、总时间及占比。
示例:
# script.py
from line_profiler import LineProfiler
@profile
def add_numbers():
total = 0
for i in range(100000):
total += i
return total
if __name__ == '__main__':
add_numbers()
然后用 kernprof -lv script.py 运行(kernprof 是 line_profiler 自带的运行器),输出:
Line # Hits Time Per Hit % Time Line Contents
==============================================================
1 @profile
2 def add_numbers():
3 1 2.0 2.0 0.0 total = 0
4 100001 25000.0 0.2 99.9 for i in range(100000):
5 100000 55000.0 0.5 0.1 total += i
6 1 2.0 2.0 0.0 return total
一眼看出循环行和累加行耗时占比。
实用技巧:
- 不需要修改代码的另一种方式:实例化
LineProfiler,手动添加要分析的函数。
lp = LineProfiler()
lp.add_function(slow_func)
lp.run('slow_func()')
lp.print_stats()
- 结合 IPython 使用更便捷:
%load_ext line_profiler,然后%lprun -f func_name func_name()。
两者的选择策略
| 场景 | 推荐工具 |
|------|----------|
| 程序刚变慢,不知哪里出问题 | cProfile 快速扫描 |
| 已经确定某个函数慢,但需要定位具体哪一行 | line_profiler |
| 生产环境临时诊断 | cProfile(标准库,零依赖) |
重要提示:分析工具本身有一定开销,line_profiler 的开销比 cProfile 大,不要在线上一直开着。通常在开发环境重现问题后,找到瓶颈即可。
实际开发中,这两个工具配合使用效率极高:先用 cProfile 把“嫌疑犯”限制到某几个函数,再用 line_profiler 揪出具体行,然后有针对性地优化算法或数据结构。这种“测量驱动优化”的方式,比靠直觉瞎改可靠得多。