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.time()去测一个网络请求的性能,结果每次都不一样。后来才发现,网络延迟把真实计算时间给淹没了。测CPU密集型任务,建议用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} 微秒")
小技巧: 在Jupyter Notebook里,直接用%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(执行次数)。

注意: line_profiler会增加运行开销,所以测出来的时间会比实际慢。但它能告诉你相对比例,这就够了。我一般先用cProfile定位到问题函数,再用line_profiler深入分析。

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(增量)。

实用场景: 我遇到过一个问题——程序跑着跑着内存就爆了。用memory_profiler一查,发现有个列表推导式生成了巨大的中间列表。改成生成器表达式后,内存占用从500MB降到了50MB。

3.6 工具选择指南

这么多工具,什么时候用哪个?我总结了一张表:

工具 适用场景 精度 开销
time模块 快速验证、粗略测量 毫秒级 极低
timeit模块 微基准测试、对比不同写法 微秒级 低
cProfile 定位瓶颈函数 函数级 中等
line_profiler 深入分析函数内部 行级 较高
memory_profiler 排查内存泄漏、优化内存 行级 较高

我的工作流一般是:先用time或timeit做快速验证,发现慢了就用cProfile定位到具体函数,再用line_profiler找到瓶颈行。如果怀疑内存问题,就上memory_profiler。

一句话总结: 别猜,要测。工具都给你准备好了,剩下的就是动手。
Python性能分析工具知识体系 性能分析 工具链 time模块 快速验证 timeit模块 微基准测试 cProfile 函数级分析 line_profiler 逐行分析 memory_profiler 内存监控 从粗到细,从快到慢,逐层深入定位性能瓶颈

好了,这五个工具够你应付大部分性能分析场景了。记住,工具只是手段,关键是养成「先测量再优化」的习惯。下次写代码的时候,不妨问问自己:这段代码真的快吗?测一下再说。