3. Python性能分析工具
性能分析这事儿,说白了就是回答三个问题:哪段代码慢?慢了多少?为什么会慢?
我刚入行那会儿,觉得代码能跑就行。直到有一次线上服务响应时间飙到10秒,查了半天才发现是某个循环里多了一次不必要的字符串拼接。嗯,从那以后,我养成了一个习惯——写代码之前先想好怎么测性能。
今天咱们聊聊Python生态里最常用的五个性能分析工具。它们各有侧重,配合起来用效果最好。
3.1 time模块——最朴素的计时器
time模块是Python自带的,不需要安装。我一般用它做快速验证,比如想知道某个函数跑了多久。
import time
def slow_function():
total = 0
for i in range(10**7):
total += i
return total
start = time.time()
result = slow_function()
end = time.time()
print(f"耗时: {end - start:.4f} 秒")
这里有个坑——time.time()返回的是墙上时间,包含CPU等待的时间。如果你的程序在等I/O,这个值会偏大。
time.process_time()。
另外,time.perf_counter()精度更高,适合微秒级的测量。我个人习惯用这个:
start = time.perf_counter()
# 你的代码
end = time.perf_counter()
print(f"耗时: {(end - start)*1000:.2f} ms")
3.2 timeit模块——精准到微秒
timeit模块是专门为性能测量设计的。它会自动关闭垃圾回收,多次运行取平均值,结果更可靠。
你想想看,手动用time模块测,一次运行可能受系统负载影响。但timeit会跑很多次,把偶然因素抹平。
import timeit
# 测量单条语句
t = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
print(f"耗时: {t:.4f} 秒")
# 测量函数
def test():
return sum(range(1000))
t = timeit.timeit(test, number=100000)
print(f"平均耗时: {t/100000*1e6:.2f} 微秒")
%timeit魔法命令更方便。它会自动选择运行次数,你只管写代码就行。
我记得有一次优化字符串拼接,用timeit对比了三种写法:
# 写法一:直接加
timeit.timeit('s = ""; for c in "hello": s += c', number=10000)
# 写法二:join
timeit.timeit('"".join(["h","e","l","l","o"])', number=10000)
# 写法三:列表推导
timeit.timeit('"".join([c for c in "hello"])', number=10000)
结果很明显,join比直接加快了将近10倍。这就是数据说话。
3.3 cProfile——函数级性能分析
当你的程序变大了,光测总耗时不够。你得知道每个函数花了多少时间。cProfile就是干这个的。
它会把程序跑一遍,记录每个函数的调用次数、总耗时、每次耗时。输出结果像这样:
import cProfile
def main():
for i in range(1000):
do_something(i)
def do_something(n):
return sum(range(n))
cProfile.run('main()', sort='cumtime')
输出结果里,ncalls是调用次数,tottime是函数自身耗时(不含子函数),cumtime是累计耗时(含子函数)。
cumtime最大的函数,那通常是瓶颈所在。然后看tottime,如果某个函数自身耗时很大,说明它内部逻辑需要优化。
我曾经优化过一个数据处理脚本,用cProfile发现有个函数被调用了100万次,每次只做了一点点事。合并调用后,性能提升了40%。
3.4 line_profiler——逐行分析
cProfile只能看到函数级别。但有时候,瓶颈在函数内部的某一行。line_profiler可以逐行告诉你每行代码的执行时间和调用次数。
安装:pip install line_profiler
用法很简单,在要分析的函数上加@profile装饰器:
@profile
def process_data(data):
result = []
for item in data:
temp = item * 2 # 这行花了多少时间?
temp = temp ** 0.5 # 这行呢?
result.append(temp) # 这行呢?
return result
process_data(range(10000))
运行:kernprof -l -v script.py
输出会显示每行代码的Time(耗时)、% Time(占比)、Hits(执行次数)。
3.5 memory_profiler——内存去哪儿了
性能不只是速度,还有内存。memory_profiler可以监控每行代码的内存使用情况。
安装:pip install memory_profiler
同样用@profile装饰器:
@profile
def create_large_list():
a = [i for i in range(100000)] # 这行用了多少内存?
b = [i*2 for i in a] # 这行呢?
return b
create_large_list()
运行:python -m memory_profiler script.py
输出会显示每行代码的Mem usage(内存使用)和Increment(增量)。
3.6 工具选择指南
这么多工具,什么时候用哪个?我总结了一张表:
| 工具 | 适用场景 | 精度 | 开销 |
|---|---|---|---|
| time模块 | 快速验证、粗略测量 | 毫秒级 | 极低 |
| timeit模块 | 微基准测试、对比不同写法 | 微秒级 | 低 |
| cProfile | 定位瓶颈函数 | 函数级 | 中等 |
| line_profiler | 深入分析函数内部 | 行级 | 较高 |
| memory_profiler | 排查内存泄漏、优化内存 | 行级 | 较高 |
我的工作流一般是:先用time或timeit做快速验证,发现慢了就用cProfile定位到具体函数,再用line_profiler找到瓶颈行。如果怀疑内存问题,就上memory_profiler。
好了,这五个工具够你应付大部分性能分析场景了。记住,工具只是手段,关键是养成「先测量再优化」的习惯。下次写代码的时候,不妨问问自己:这段代码真的快吗?测一下再说。