前言
「高性能」是一个目标,不是一剂固定的药方。写高性能 Python 的核心能力,其实不是记住某几行「神级优化代码」,而是会做策略选择:先判断瓶颈属于哪一类,再选出对应的解法。选错了方向,再精细的写法调整也只是白费。
本文给出一张「策略全景图」:从测量、到并发模型的选择、到数据结构与内存、再到何时该把工作交给 C 层。全程只讲原理与取舍,不下任何性能结论——快慢取决于你的场景,请用timeit与cProfile自己测。本文不给出、也不复述任何性能倍数。
需要声明:本机没有 Python 解释器,也无编译工具链,本文代码无法在本环境运行,均为按官方文档整理的写法,请以官方文档为准;本文不声称任何代码被实际执行过。
一、第一步永远是定位瓶颈
用时序测量比较写法
timeit适合比较「同一任务的不同写法」。
# 适用于 Python 3.8+
import timeit
print(timeit.timeit("sorted([3, 1, 2])", number=1000))timeit.timeit(stmt, setup='pass', timer=..., number=1000000, globals=None)返回执行总秒数。它的特点是关掉垃圾回收干扰、重复多次,比手写计时可靠。
用剖析器看整个程序
timeit只看一小段。要看整个程序里谁最耗时,用cProfile。
# 适用于 Python 3.8+
import cProfile
def work():
return sum(i * i for i in range(1000))
cProfile.run("work()")也可以从命令行剖析一个脚本,按累计耗时排序输出:
# 适用于任意已安装的 Python 3
python -m cProfile -s cumtime 脚本名.py先定位、再动手,是整篇的前提。没有测量的优化都是猜测。
二、策略选择:先分清瓶颈类型
瓶颈大致分三类,对应的解法完全不同。
| 瓶颈类型 | 特征 | 优先策略 |
|---|
| CPU 密集 | 大量纯计算,CPU 占满 | 多进程、算法改进、下沉到 C 层 |
| I/O 密集 | 大量等待网络 / 磁盘 | 多线程、异步 I/O |
| 内存 / 对象开销 | 内存吃紧、对象数量巨大 | 生成器、紧凑容器、减少对象 |
选错类型的例子很典型:给一段纯计算套多线程,几乎不会带来并行收益;反过来,给大量网络请求去换多进程,进程的启动与通信开销又可能盖过收益。
CPU 密集:多进程
因为 GIL 的存在,同一进程内同一时刻只有一个线程执行字节码,多线程无法让纯计算并行。想要真正并行,用多进程;或者把热点逻辑用 C 扩展实现。
# 适用于 Python 3.8+
from concurrent.futures import ProcessPoolExecutor
def square(n):
return n * n
if __name__ == "__main__":
with ProcessPoolExecutor() as ex:
print(list(ex.map(square, range(5))))ProcessPoolExecutor来自标准库concurrent.futures,map返回的是迭代器。注意进程之间不共享内存,要传递的数据必须能被序列化(pickle)。
I/O 密集:多线程或异步
线程在等待 I/O 时会释放 GIL,所以等待型任务用线程是合理的。
# 适用于 Python 3.8+
from concurrent.futures import ThreadPoolExecutor
def fetch(n):
# 真实场景里这里是网络请求,这里用一个纯函数占位
return n + 1
with ThreadPoolExecutor() as ex:
print(list(ex.map(fetch, range(5))))异步 I/O(asyncio)是另一种路线,用单线程事件循环处理大量并发等待,省去线程切换。它的接口在 3.8 到 3.12 之间有过调整,写新代码建议用入口函数asyncio.run来驱动顶层协程,而不是依赖可能在无运行中循环时发出弃用警告的旧接口。
# 适用于 Python 3.8+
import asyncio
async def main():
await asyncio.sleep(0)
return "done"
print(asyncio.run(main()))关于 GIL 的准确表述
- 同一进程内,同一时刻只有一个线程执行 Python 字节码。
- 因此多线程对 CPU 密集任务没有并行加速;对 I/O 密集任务有效。
- CPython 3.13 起有实验性的自由线程(无 GIL)构建,但默认不启用,需要单独安装。不能说「Python 已经没有 GIL 了」。
三、数据与算法的选择
在写法和并发之前,先确认算法与数据结构选对了。这是收益最大、也最容易被忽略的一层。
| 需求 | 选择 | 原因 |
|---|
| 成员判断 | set/dict | 哈希查找替代线性扫描 |
| 需要顺序且元素少 | list | 简单直接 |
| 计数 | collections.Counter | 专为计数设计 |
| 分组 | collections.defaultdict | 免去建键分支 |
| 只需遍历一次 | 生成器 | 不构造整份数据 |
# 适用于 Python 3.8+
from collections import Counter, defaultdict
c = Counter("mississippi")
print(c["s"])
group = defaultdict(list)
for n in range(10):
group[n // 3].append(n)
print(dict(group))四、把工作下沉到 C 层
Python 的内置函数和容器方法用 C 实现,调用它们等于把循环交给 C;这通常比在 Python 层面手写循环更省开销。
# 适用于 Python 3.8+
data = [3, 1, 4, 1, 5]
print(sum(data)) # 聚合下沉到 C
print("".join(str(x) for x in data)) # 拼接用 joinarray与memoryview提供更紧凑的数值存储与零复制视图:
# 适用于 Python 3.8+
from array import array
nums = array("l", range(5)) # "l" 表示有符号长整型
print(nums[2], len(nums))
view = memoryview(b"abc")
print(view[1])再往下就是__slots__降低每实例开销,以及把热点逻辑写成 C 扩展——但后者需要编译工具链,接口与实现细节以 CPython 官方文档为准。至于用 NumPy 这类第三方库做向量化,是常见路线,其 API 以该库官方文档为准。
# 适用于 Python 3.8+
class Reading:
__slots__ = ("sensor", "value")
def __init__(self, sensor, value):
self.sensor = sensor
self.value = value
r = Reading("t1", 21.5)
print(r.sensor, r.value)五、减少重复工作
- 循环不变量提到循环外。
- 对相同参数的重复调用做记忆化(
functools.lru_cache)。 - 正则编译一次、复用。
- 避免在循环里做能提前算好的事情。
# 适用于 Python 3.8+
from functools import lru_cache
@lru_cache(maxsize=256)
def cost(n):
return n * n + 1
print([cost(i // 2) for i in range(20)])常见坑点
1. 不测量就选策略。
- ❌ 一上来就上多进程,结果瓶颈其实是磁盘 I/O。
- ✅ 先用
cProfile与timeit定位瓶颈类型。
2. 给 CPU 密集任务用多线程。
- ❌ 期望线程能带来并行加速,受 GIL 限制却看不到效果。
- ✅ CPU 密集用
multiprocessing/ProcessPoolExecutor。
3. 说「Python 已经没有 GIL 了」。
- ❌ 把 3.13 的实验性自由线程构建当成默认能力。
- ✅ 默认构建仍有 GIL,自由线程需单独构建、默认不启用。
4. 用多进程却传不可序列化的对象。
- ❌ 把打开的文件、锁之类传进子进程,直接报序列化错误。
- ✅ 只传可 pickle 的简单数据,或改用线程 / 异步。
5. 忽略算法的复杂度。
- ❌ 花大力气微调写法,却没发现用的是线性查找。
- ✅ 优先换更合适的数据结构与算法。
6. 迷信第三方库就一定会更快。
- ❌ 无脑引入重型依赖,小数据上反而更慢还更难维护。
- ✅ 先测量再决定是否引入;小任务用标准库往往更合适。
7. 过度优化可读性换来的收益。
- ❌ 把代码改得晦涩,维护成本远超那点收益。
- ✅ 优化要可度量、可回退,别牺牲可维护性。
8. 忘了lru_cache的代价。
- ❌ 参数取值几乎不重复时仍上缓存,只增内存不增命中。
- ✅ 确认存在「相同参数重复调用」再用。
总结
| 环节 | 策略 | 判断依据 |
|---|
| 定位 | timeit/cProfile | 先量后改 |
| 并发 | 多进程 / 多线程 / 异步 | 瓶颈是 CPU 还是 I/O |
| 数据 | 集合、字典、Counter | 查找与聚合的性质 |
| 下沉 | 内置函数、array、__slots__ | 把循环交给 C 层 |
| 去重 | 提到循环外、lru_cache | 相同结果不重复算 |
| 约束 | GIL 仍在,自由线程是实验性 | 默认构建的边界 |
高性能 Python 的关键在「选」而不在「炫」:先测出瓶颈类型,再选并发模型、数据结构与实现层次。本文通篇不给性能数字,因为在你的具体场景里,唯一有意义的那组数字,得由你自己用timeit和cProfile跑出来。