ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python高性能编程中的策略选择与全景实战指南

Python高性能编程中的策略选择与全景实战指南

前言


「高性能」是一个目标,不是一剂固定的药方。写高性能 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)) # 拼接用 join

array与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跑出来。




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进