Python 性能优化全攻略:提升 3 倍执行速度的实用技巧
这篇文章不谈玄学,不堆概念。从实际工程出发,带你把 Python 程序从“能跑”优化到“跑得快”,并且知道为什么快。
一、先别优化:定位瓶颈才有意义
很多人一上来就:
- 改成多线程
- 换成异步
- 上缓存
- 用 C 扩展
结果性能没提升,复杂度却上去了。
1️⃣ 性能优化第一原则:量化
没有数据的优化都是猜测。
推荐工具:
time:简单测量耗时cProfile:函数级分析line_profiler:行级分析memory_profiler:内存分析
示例:cProfile 分析
import cProfile def slow_function(): total = 0 for i in range(10_000_000): total += i return total cProfile.run("slow_function()") 输出结果中重点关注:
- 调用次数(ncalls)
- 单次耗时(percall)
- 总耗时(tottime)
经验:80% 的时间通常集中在 20% 的代码上。
二、算法优化:性能提升的天花板
如果你用的是 O(n²) 算法,换成 O(n log n),
性能提升远大于任何“语法优化”。
示例:列表查找优化
错误写法:
for item in big_list: if item in another_list: ... 时间复杂度:
O(n²)
优化写法:
another_set = set(another_list) for item in big_list: if item in another_set: ... 时间复杂度:
O(n)
这种改动往往是“数量级”的提升。
算法复杂度示意图
O(1) 常数级 O(log n) 对数级 O(n) 线性 O(n log n) O(n²) O(2^n) 如果程序慢,先问:
是代码慢,还是算法慢?
三、数据结构选型决定性能上限
Python 内置结构性能差异非常大:
| 结构 | 查找 | 插入 | 适用场景 |
|---|---|---|---|
| list | O(n) | O(1) 末尾 | 顺序数据 |
| dict | O(1) | O(1) | 快速查找 |
| set | O(1) | O(1) | 去重判断 |
| deque | O(1) 头尾 | O(1) | 队列 |
例如:
# 不要频繁在 list 头部插入 my_list.insert(0, x) # 使用 deque from collections import deque dq = deque() dq.appendleft(x) 在高频操作中,这种结构替换能带来 2~5 倍差异。
四、循环优化与内置函数
Python 的 for 循环本身不快。
1️⃣ 使用内置函数代替循环
慢:
total = 0 for i in numbers: total += i 快:
total = sum(numbers) 原因:
sum() 在 C 层实现,比 Python 循环快。
2️⃣ 列表推导优于 append
慢:
result = [] for x in data: result.append(x * 2) 快:
result = [x * 2 for x in data] 通常快 20%~40%。
五、避免不必要的对象创建
Python 对象创建成本不低。
错误示例:
for i in range(1_000_000): s = str(i) 优化方式:
是否必须转字符串?是否可以批量处理?对象越多,GC 压力越大。
六、字符串拼接优化
慢:
s = "" for i in range(10000): s += str(i) 快:
s = "".join(str(i) for i in range(10000)) 原因:
字符串不可变,+= 会不断创建新对象。
七、并发优化:什么时候有用?
这里要说清楚一个核心问题:
Python 有 GIL。
CPU 密集型任务:
- 多线程 ≈ 无效
IO 密集型任务:
- 多线程 / asyncio 有效
CPU 密集型 → 多进程
from multiprocessing import Pool def task(x): return x * x with Pool(4) as p: result = p.map(task, range(10_000)) IO 密集型 → asyncio
import asyncio async def task(): await asyncio.sleep(1) asyncio.run(task()) 优化方向一定要匹配任务类型。
八、使用 C 加速工具
如果纯 Python 不够:
- NumPy(向量化)
- Cython
- Numba
- PyPy
示例:NumPy 向量化
慢:
result = [] for i in range(len(a)): result.append(a[i] + b[i]) 快:
import numpy as np result = np.array(a) + np.array(b) 向量化常常是 10 倍级提升。
九、缓存机制
重复计算是性能杀手。
from functools import lru_cache @lru_cache(maxsize=128) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) 递归性能可提升数百倍。
十、内存优化
性能不只是 CPU。大对象及时释放;使用生成器代替大列表;
慢:
data = [i for i in range(10_000_000)] 快:
data = (i for i in range(10_000_000)) 生成器节省大量内存。
十一、优化优先级建议
按收益排序:
1️⃣ 算法优化
2️⃣ 数据结构选择
3️⃣ 内置函数替换
4️⃣ 向量化
5️⃣ 缓存
6️⃣ 并发
7️⃣ C 扩展
不要反过来。
十二、真实优化案例
某数据处理程序:
- 原始版本:18 秒
- 优化后:5.8 秒
优化步骤:
- 用 cProfile 找到 70% 时间在 list 查找
- 改为 set
- 替换字符串拼接为 join
- 使用 NumPy 向量化计算
没有改架构,没有加服务器。
十二、真实优化案例
真实优化案例:一次“看似普通”的数据清洗任务,是怎么从 47 秒优化到 6.3 秒的
这个案例发生在一个很普通的项目里,没有什么高并发,没有分布式,没有大模型。
就是一个典型的“数据清洗 + 规则匹配”的后台脚本。
但问题是——每天要跑几百次。
最开始没人觉得慢,直到它开始影响后续流程。
一、背景
业务场景很简单:
- 读取一份约 30 万行的 CSV 文件
- 对每一行做字段清洗
- 根据规则表进行匹配
- 生成一份结果文件
单次运行时间:47 秒
听起来不算灾难,但:
- 每天执行 300+ 次
- 串行流程,后面任务要等它
于是我接手做优化。
二、第一步:不要猜,先量化
我第一反应不是改代码,而是:
import cProfile cProfile.run("process_data()") 结果非常清晰:
- 71% 时间耗在一个函数
match_rules - 19% 时间耗在字符串拼接
- 其他可以忽略
这已经很好了——
有明确突破口。
三、问题一:规则匹配的灾难性写法
原始代码大概是这样:
def match_rules(row, rules): for rule in rules: if rule["keyword"] in row["content"]: return rule["category"] return "unknown" 问题在哪里?
rules有 5000 条- 每一行都要循环 5000 次
- 总行数 300,000
复杂度接近:
300,000 × 5,000 = 15 亿次字符串匹配 这不是“慢”,是结构性错误。
四、第一次优化:构建索引结构
我做的第一件事:
把规则按关键词长度分组。
但效果不明显。
后来我意识到一个核心问题:
我不需要遍历规则,我需要“反向查找”。
于是我改成了:
# 预处理阶段 keyword_map = {} for rule in rules: keyword_map[rule["keyword"]] = rule["category"] 但问题仍然存在:
in 匹配仍然需要遍历关键词。
五、真正有效的优化:使用 Aho-Corasick 算法
这个场景本质是“多关键词匹配”。
我引入了 pyahocorasick:
import ahocorasick automaton = ahocorasick.Automaton() for rule in rules: automaton.add_word(rule["keyword"], rule["category"]) automaton.make_automaton() 匹配变成:
def match_rules(content): for _, category in automaton.iter(content): return category return "unknown" 复杂度从:
O(规则数 × 文本长度)
变成:
O(文本长度)
运行时间直接从 47 秒降到 14 秒。
这一步是核心突破。
六、第二个问题:字符串拼接
原代码:
result = "" for row in data: result += process(row) + "\n" 典型错误。
我改成:
lines = [] for row in data: lines.append(process(row)) result = "\n".join(lines) 运行时间从 14 秒 → 9.8 秒
七、第三个问题:CSV 读取方式
原来使用的是:
import csv reader = csv.DictReader(open("data.csv")) 我测试了 pandas:
import pandas as pd df = pd.read_csv("data.csv") 在这个数据规模下:
- pandas 更快
- 且字段操作更方便
最终整体时间 → 7.4 秒
八、最后一步:减少函数调用开销
在 profile 里我发现:
process(row) 被调用 30 万次。
内部大量:
.strip().lower()- 重复字段转换
我把清洗逻辑内联,并提前做一次性转换:
原来:
def clean(text): return text.strip().lower() 后来:
content = row["content"].strip().lower() 减少函数栈开销。
最终时间:
6.3 秒
九、优化过程总结
| 阶段 | 时间 |
|---|---|
| 原始版本 | 47s |
| Aho-Corasick | 14s |
| 字符串 join | 9.8s |
| pandas 读取 | 7.4s |
| 减少函数调用 | 6.3s |
总提升:
7.5 倍
十、关键体会
这次优化让我重新确认几件事:
1️⃣ 性能瓶颈通常是结构问题
不是“Python 慢”,
而是你写成了 O(n²)。
2️⃣ 算法 > 语法优化
字符串拼接带来 4 秒提升
算法改造带来 33 秒提升
优先级非常明显。
3️⃣ profile 是必须的
如果没有 profile,我可能会:
- 盲目加多线程
- 改成 asyncio
- 升级服务器
这些都是错误方向。
4️⃣ 小优化叠加才有大效果
单个改动不一定震撼。
但连续五个小改动,效果非常明显。
十一、如果再优化还能做什么?
我后来测试过:
- 多进程拆分文件
- Cython 重写核心匹配
还能再降 1~2 秒。
但业务已经满足要求,我就停了。
优化要有边界。
性能优化本质上是一种:
对程序运行路径的尊重。