第一原则:先测,再优化
"Premature optimization is the root of all evil." — Donald Knuth
不要凭感觉优化。先用 cProfile 找瓶颈,针对真正慢的地方下手。
timeit:测一段代码的速度
import timeit
# 比较两种写法
t1 = timeit.timeit("[x*2 for x in range(1000)]", number=10000)
t2 = timeit.timeit("list(map(lambda x: x*2, range(1000)))", number=10000)
print(t1, t2)
或在终端:
python -m timeit "[x*2 for x in range(1000)]"
# 1000 loops, best of 5: 230 usec per loop
适合对比小段代码。
cProfile:扫描整个程序
python -m cProfile -s cumulative myapp.py
输出每个函数的调用次数、累计耗时,按 cumulative 排序——耗时最长的在最上面。
更直观:用 snakeviz 可视化:
pip install snakeviz
python -m cProfile -o profile.dat myapp.py
snakeviz profile.dat
浏览器里打开火焰图——一眼看出瓶颈。
数据结构选择决定 80% 性能
# 慢:list 的 in 是 O(n)
huge = list(range(1_000_000))
999_999 in huge # 扫一遍,~10 ms
# 快:set 的 in 是 O(1)
huge_set = set(huge)
999_999 in huge_set # 哈希查找,~50 ns
# 200,000 倍差距
对应规则:
| 操作 | list | set / dict |
|---|---|---|
| 查找 in | O(n) | O(1) |
| 增删 | O(n) | O(1) |
| 索引访问 | O(1) | 不支持 |
经常 in 检查的容器一律用 set。
字符串拼接
# 慢:每次拼接都创建新字符串
s = ""
for x in items:
s += str(x) # O(n²)!
# 快:一次性 join
s = "".join(str(x) for x in items) # O(n)
局部变量比全局快
热循环里用局部变量:
import math
# 慢
for x in range(1_000_000):
y = math.sqrt(x)
# 快
sqrt = math.sqrt # 缓存到局部
for x in range(1_000_000):
y = sqrt(x)
不大但有时候有用。
内置函数 / 内置类型 vs 手写
sum / map / filter / sorted / set 都是 C 实现的——比纯 Python 循环快几倍。
# 慢
total = 0
for x in nums:
total += x
# 快
total = sum(nums)
大量数值计算用 NumPy
纯 Python 处理百万数字会慢;NumPy 向量化操作快 100 倍:
import numpy as np
a = np.array(huge_list)
result = a ** 2 + 3 * a + 1 # 一行向量化
缓存 lru_cache
讲过——纯函数加 @lru_cache 立刻变快。
警惕这些"慢操作"
| 操作 | 替代 |
|---|---|
list.insert(0, x) |
collections.deque.appendleft |
list.pop(0) |
deque.popleft |
重复 str + str |
"".join |
| 循环里查找 list | 提前转 set |
| 大数据循环 + 数学 | NumPy |
何时不要优化
如果代码已经够快、跑得不频繁,别花时间优化。可读性 > 性能。
记住:
- 先 profile——别瞎猜
- 改数据结构——立竿见影
- 用内置函数——免费的快
- 再想算法——O(n²) → O(n log n)
- 最后才想 C 扩展 / Cython——99% 用不到
下一篇讲内存管理。