第10章 10.4 性能分析与优化
🎯 开场 3 分钟:为什么你的程序跑得比蜗牛还慢?
上一章我们学会了怎么把自己写的工具发布到 PyPI,让全世界的人都能用 pip install 安装你的作品。太酷了!
但问题来了——你写的代码发布出去后,用户跑起来发现:慢!卡!等半天没反应!
这就是今天要解决的问题。
举个例子:你写了一个自动处理 10 万条数据的脚本,本地测试几条数据飞快,结果一跑真实数据,等了 5 分钟还在转圈圈。这种事儿,十有八九的新手都遇到过。
学完这章,你就能:
- 定位代码里到底哪一行在拖后腿
- 量化到底慢了多少秒(而不是「感觉有点慢」)
- 优化让程序快上 10 倍甚至 100 倍
🧱 基础 25 分钟:性能分析的核心概念
概念 1:timeit —— 你的随身秒表
是什么:Python 内置的计时工具,就像你手机上的秒表。
为什么要用:想知道某段代码到底跑了多久,精确到微秒。
怎么用:
import timeit
# 测量执行 1000 次需要多久
结果 = timeit.timeit('sum(range(1000))', number=1000)
print(f"1000 次合计耗时: {结果:.4f} 秒")
# 单次执行时间
单次 = timeit.timeit('x = [i**2 for i in range(100)]', number=1)
print(f"单次耗时: {单次:.6f} 秒")
输出:
1000 次合计耗时: 0.0234 秒
单次耗时: 0.000089 秒
说白了:timeit 就是个高精度的秒表,专门用来测量代码跑一次/跑 N 次要多久。

概念 2:cProfile —— 代码的 X 光片
是什么:Python 内置的性能分析器,能告诉你「哪个函数被调用了多少次、花了多少时间」。
为什么要用:timeit 只能告诉你「总时间」,但 cProfile 能告诉你「具体哪一行/哪个函数最慢」。
怎么用:
import cProfile
import pstats
import io
def慢函数():
total = 0
for i in range(10000):
total += i ** 2
return total
def快函数():
return sum(i ** 2 for i in range(10000))
# 创建一个性能分析器
profiler = cProfile.Profile()
profiler.enable()
# 运行你要分析的代码
结果1 = 慢函数()
结果2 = 快函数()
profiler.disable()
# 把分析结果打印出来
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.print_stats() # 打印所有统计信息
print(stream.getvalue())
输出:
10004 function calls in 0.008 seconds
rdered by: cumulative time
calls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.008 0.008 10_4_性能分析.py:11(快函数)
1 0.005 0.005 0.005 0.005 10_4_性能分析.py:7(慢函数)
说白了:cProfile 就是代码的体检报告,告诉你哪个器官(函数)有问题。
概念 3:line_profiler —— 逐行扫描的显微镜
是什么:第三方库,能逐行分析代码执行时间,精确到毫秒级。
为什么要用:cProfile 告诉你哪个函数慢,line_profiler 告诉你函数里哪一行最慢。
怎么用:
先安装:
pip install line_profiler
然后用 @profile 装饰器标记要分析的函数:
# my_script.py
from line_profiler import profile
@profile
def分析你的代码():
数据 = list(range(10000))
# 第一步:过滤偶数
过滤后 = [x for x in 数据 if x % 2 == 0]
# 第二步:平方
平方后 = [x ** 2 for x in 过滤后]
# 第三步:求和
结果 = sum(平方后)
print(f"总和是: {结果}")
if __name__ == "__main__":
分析你的代码()
运行:
kernprof -l -v my_script.py
输出:
Timer unit: 1e-06 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
5 @profile
6 def 分析你的代码():
7 1 48 48.0 0.2 数据 = list(range(10000))
8 1 1204 1204.0 5.1 过滤后 = [x for x in 数据 if x % 2 == 0]
9 1 20987 20987.0 88.7 平方后 = [x ** 2 for x in 过滤后]
10 1 1340 1340.0 5.9 结果 = sum(平方后)
看!第 9 行(平方操作)吃了 88.7% 的时间,这就是你要优化的目标!

概念 4:内存分析 —— 看看你的代码吃了多少内存
是什么:分析你的程序用了多少内存,内存泄漏在哪里。
怎么用(用 memory_profiler):
pip install memory_profiler
from memory_profiler import profile
@profile
def吃内存的函数():
# 创建一个大列表
大数据 = [i ** 2 for i in range(1000000)]
return sum(大数据)
if __name__ == "__main__":
结果 = 吃内存的函数()
print(f"结果是: {结果}")
运行:
python -m memory_profiler 你的文件.py
概念 5:常见的性能瓶颈(记住这 4 个)
| 瓶颈类型 | 举例子 | 怎么改 |
|---|---|---|
| 循环里做计算 | for i in data: result.append(i**2) |
用列表推导式或 map |
| 重复创建对象 | for i in range(100): obj = MyClass() |
移到循环外面 |
| 字符串拼接 | s += "a" 循环 1 万次 |
用 ''.join(list) |
| 不必要的复制 | new_list = old_list[:] |
直接用切片或引用 |
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):给任意函数装上计时器
场景:你写了好几个函数,想知道每个跑多久。
完整代码:
import time
from functools import wraps
def计时器(函数):
"""装饰器:给函数加上执行时间统计"""
@wraps(函数)
def包装函数(*args, **kwargs):
开始 = time.time()
结果 = 函数(*args, **kwargs)
结束 = time.time()
print(f"【{函数.__name__}】耗时 {结束 - 开始:.4f} 秒")
return 结果
return 包装函数
# 使用方式:加个 @计时器 就搞定
@计时器
def处理数据(数据量):
return sum(i ** 2 for i in range(数据量))
@计时器
def找最大值(数据量):
数据 = list(range(数据量))
return max(数据)
# 测试一下
print("测试 10000 条数据:")
处理数据(10000)
找最大值(10000)
print("\n测试 100000 条数据:")
处理数据(100000)
找最大值(100000)
预期输出:
测试 10000 条数据:
【处理数据】耗时 0.000892 秒
【找最大值】耗时 0.001234 秒
测试 100000 条数据:
【处理数据】耗时 0.008901 秒
【找最大值】耗时 0.012345 秒
一句话解释:用装饰器把函数包一层,自动计时,打印出来。
项目 2(15 分钟):分析 CSV 数据处理哪里慢
场景:你写了个脚本从 CSV 读取 1 万行数据,做清洗和统计,但跑得很慢。
数据文件 sales.csv(自己创建一个):
日期,商品,销量,单价
2024-01-01,苹果,100,5.5
2024-01-01,香蕉,80,3.2
2024-01-01,橙子,120,4.8
...(自行生成更多数据)
完整代码:
import csv
import cProfile
import pstats
import io
from collections import defaultdict
def读取数据(文件名):
"""从 CSV 读取数据"""
数据 = []
with open(文件名, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
数据.append(row)
return 数据
def清洗数据(原始数据):
"""清洗数据:转成正确类型"""
清洗后 = []
for row in 原始数据:
try:
清洗后.append({
'日期': row['日期'],
'商品': row['商品'],
'销量': int(row['销量']),
'单价': float(row['单价'])
})
except (ValueError, KeyError):
continue # 跳过有问题的行
return 清洗后
def按商品统计(数据):
"""按商品汇总销售额"""
统计 = defaultdict(lambda: {'销量': 0, '销售额': 0})
for row in 数据:
商品 = row['商品']
统计[商品]['销量'] += row['销量']
统计[商品]['销售额'] += row['销量'] * row['单价']
return dict(统计)
def性能分析():
"""用 cProfile 分析整个流程"""
profiler = cProfile.Profile()
profiler.enable()
# 跑一遍完整流程
原始 = 读取数据('sales.csv')
清洗后 = 清洗数据(原始)
统计结果 = 按商品统计(清洗后)
profiler.disable()
# 打印分析报告
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.print_stats(10) # 只显示前 10 行
print("=== 性能分析报告 ===")
print(stream.getvalue())
return 统计结果
# 运行
if __name__ == "__main__":
结果 = 性能分析()
print("\n=== 统计结果 ===")
for 商品, 数据 in 结果.items():
print(f"{商品}: 销量={数据['销量']}, 销售额={数据['销售额']:.2f}")
预期输出:
=== 性能分析报告 ===
10004 function calls in 0.452 seconds
rdered by: cumulative time
calls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.452 0.452 10_4_demo.py:48(性能分析)
1 0.003 0.003 0.380 0.380 10_4_demo.py:8(读取数据)
1 0.180 0.180 0.340 0.340 10_4_demo.py:17(清洗数据)
1 0.050 0.050 0.065 0.065 10_4_demo.py:28(按商品统计)
=== 统计结果 ===
苹果: 销量=10000, 销售额=55000.00
香蕉: 销量=8000, 销售额=25600.00
...
一句话解释:用 cProfile 一跑就知道,清洗数据 这个函数最慢(吃了 0.38 秒),可能需要优化。
项目 3(15 分钟):做个「代码跑得慢?让我来诊断」小工具
场景:做一个交互式工具,用户粘贴代码,它告诉你哪里可能慢。
完整代码:
import timeit
import cProfile
import pstats
import io
import sys
class性能诊断器:
def __init__(self):
self.函数耗时 = {}
def诊断(self, 代码字符串, 运行环境=None):
"""诊断一段代码的性能问题"""
print("=" * 50)
print("🔍 性能诊断开始")
print("=" * 50)
# 1. 基本计时
print("\n📊 【基本计时】")
try:
单次 = timeit.timeit(代码字符串, number=1, globals=运行环境)
千次 = timeit.timeit(代码字符串, number=1000, globals=运行环境)
print(f" 单次执行: {单次*1000:.2f} 毫秒")
print(f" 1000次执行: {千次*1000:.2f} 毫秒")
print(f" 平均每次: {千次:.4f} 秒")
except Exception as e:
print(f" ❌ 计时失败: {e}")
# 2. 函数级分析
print("\n📊 【函数调用分析】")
try:
# 用 cProfile 分析
profiler = cProfile.Profile()
profiler.enable()
# 执行代码
exec(代码字符串, 运行环境 or {})
profiler.disable()
# 打印结果
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats(8) # 前 8 行
print(stream.getvalue())
except Exception as e:
print(f" ❌ cProfile 失败: {e}")
print("=" * 50)
print("💡 优化建议")
print(" - 如果某函数调用次数很多,考虑减少调用")
print(" - 如果某函数 cumtime 很高,考虑优化算法")
print(" - 检查是否有循环内重复计算")
print("=" * 50)
# 使用例子
if __name__ == "__main__":
诊断器 = 性能诊断器()
# 测试代码
测试代码 = """
总数 = 0
for i in range(10000):
总数 += i ** 2
"""
print("这段代码快不快?")
print(测试代码)
诊断器.诊断(测试代码)
预期输出:
这段代码快不快?
总数 = 0
for i in range(10000):
总数 += i ** 2
==================================================
🔍 性能诊断开始
==================================================
📊 【基本计时】
单次执行: 0.89 毫秒
1000次执行: 892.34 毫秒
平均每次: 0.000892 秒
📊 【函数调用分析】
10004 function calls in 0.001 seconds
rdered by: cumulative time
calls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 <string>:3(<module>)
==================================================
💡 优化建议
- 如果某函数调用次数很多,考虑减少调用
- 如果某函数 cumtime 很高,考虑优化算法
- 检查是否有循环内重复计算
==================================================
一句话解释:把 timeit + cProfile + 优化建议打包成一个小工具,输入代码字符串就能自动诊断。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:循环里用 + 拼接字符串
# ❌ 错误示范:循环 1 万次拼接,超慢
结果 = ""
for i in range(10000):
结果 += str(i)
# ✅ 正确示范:用 join
结果 = "".join(str(i) for i in range(10000))
原因:每次 += 都会创建新字符串、复制旧内容,O(n²) 复杂度。join 一次搞定,O(n)。
坑 2:在循环里创建大对象
# ❌ 错误示范:循环 1000 次创建列表
for i in range(1000):
临时列表 = [j ** 2 for j in range(10000)] # 每次都重新分配内存
总和 += sum(临时列表)
# ✅ 正确示范:只创建一次
大数据 = [j ** 2 for j in range(10000)]
for i in range(1000):
总和 += sum(大数据)
坑 3:以为「列表推导式一定比循环快」
# ❌ 错误示范:复杂逻辑硬塞进列表推导式,反而更慢
结果 = [math.sqrt(x) if x > 0 else 0 for x in 数据 if x % 2 == 0 and func(x)]
# ✅ 正确示范:简单逻辑用推导式,复杂逻辑用普通循环
结果 = []
for x in 数据:
if x % 2 == 0:
if x > 0:
结果.append(math.sqrt(x))
else:
结果.append(0)
坑 4:用 list(range(...)) 而不是 range(...) 直接用
# ❌ 错误示范:转成列表,白占内存
数据 = list(range(10000000))
# ✅ 正确示范:range 对象直接用,不占额外内存
数据 = range(10000000)
原因:range(1000) 只是个懒对象,不占内存;但 list(range(1000)) 要真实分配内存。
坑 5:忘记 lru_cache 缓存
# ❌ 错误示范:递归计算斐波那契,每次都重算
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# ✅ 正确示范:加缓存,飞快
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
性能小贴士:用 __slots__ 减少内存占用
如果你要创建大量对象(比如 100 万个小对象),用 __slots__ 可以省很多内存:
# 普通类:每个实例有自己的 __dict__,占 ~100 字节
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 用 __slots__:没有 __dict__,占 ~50 字节(省一半)
class Person:
__slots__ = ['name', 'age']
def __init__(self, name, age):
self.name = name
self.age = age
调试技巧:用 print 打日志定位问题
import time
def处理数据(数据列表):
总数 = len(数据列表)
print(f"[DEBUG] 开始处理,共 {总数} 条数据")
开始 = time.time()
结果 = []
for i, item in enumerate(数据列表):
if i % 1000 == 0:
print(f"[DEBUG] 已处理 {i}/{总数}")
结果.append(item ** 2)
结束 = time.time()
print(f"[DEBUG] 处理完成,耗时 {结束-开始:.2f} 秒")
return 结果
关键:加几个
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):给函数加计时器
- 输入:有一个函数 def 求和(n): return sum(range(n))
- 预期输出:打印出函数耗时
- 提示:用项目 1 的 @计时器 装饰器
练习 2(2 分钟):找出最慢的那一行
- 输入:用 line_profiler 分析这段代码:
def test():
a = [i for i in range(1000)]
b = [i**2 for i in a]
c = sum(b)
return c
- 预期输出:说出哪一行最慢
- 提示:看
% Time列
练习 3(2 分钟):修复字符串拼接
- 输入:for i in range(1000): s += str(i) 很慢
- 预期输出:写出优化后的版本
- 提示:用 join
练习 4(2 分钟):用 lru_cache 加速
- 输入:计算 fib(30) 的递归函数,超级慢
- 预期输出:加速后的代码(加 @lru_cache)
- 提示:加一行装饰器
练习 5(2 分钟):读性能分析报告
- 输入:以下 cProfile 输出,说出哪个函数最慢:
ncalls tottime cumtime
00 0.50 1.20 func_a
10 0.30 0.80 func_b
1 0.10 2.10 func_c
- 预期输出:
func_c最慢,因为 cumtime 最大 - 提示:看
cumtime列,不是tottime
作业题(30 分钟 - 2 小时)
作业:做一个「第 10 章 10.4 性能监控小助手」
需求描述:做一个命令行工具,能分析任意 Python 文件的性能。
功能点:
1. 接收一个 .py 文件路径作为参数
2. 自动运行 cProfile 分析
3. 找出耗时最长的 3 个函数
4. 给出优化建议
加分项:
1. 支持 --output 参数把报告保存到文件
2. 支持 --top N 参数指定显示前 N 个函数
验收标准:
- 能跑起来:python profiler.py my_script.py
- 输出包含函数名、调用次数、耗时
- 代码有注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章 3 个核心点
- timeit 给你精确到微秒的计时,cProfile 给你函数级别的分析,line_profiler 给你逐行的显微镜
- 性能瓶颈就那几个:循环里计算、重复创建对象、字符串拼接、不必要的复制
- 优化口诀:能用推导式就不用循环,能缓存就缓存,能一次搞定就别分多次
延伸学习资源
- Python 官方文档 - timeit —— 官方出品,必属精品
- Python 官方文档 - cProfile —— 详细到你会爱上它
- 《Python 性能编程》—— 书如其名,专门讲 Python 性能优化
互动钩子:你在写代码时遇到过「明明数据不多,但跑起来奇慢无比」的情况吗?最后怎么解决的?评论区聊聊,老粉优先回复!
📢 下章预告:学会了性能分析,下一章我们要做一个真正的命令行工具,把你写的程序打包成
pip install就能用的那种。敬请期待!

评论(0)