前言「Python 慢」是一个流传很广的结论但很多人对它只有感觉、没有理解。常见的误解有两类一类是把它归因于「解释器写得差」另一类是以为「多开几个线程就快了」。这两个说法都不准确。准确的说法是Python这里指 CPython 实现的慢主要来自它的执行模型——先编译成字节码再由虚拟机逐条解释执行同时它是动态类型语言每次操作都要在运行时确定对象的类型。这两点决定了它把很多工作留到了运行时。这跟「写得差」不是一回事它是一种设计取舍换来的是灵活和开发效率。本文会分三层讲先讲执行模型为什么导致额外开销再讲 GIL全局解释器锁划定了并发的边界最后给出一套按层面组织的优化方案。全文不会给出任何未经测量的性能倍数——具体到你的场景快多少请用timeit自己测。需要声明本机没有 Python 解释器本文代码均无法在本环境实际执行都是按 Python 官方文档整理的写法请以官方文档为准。本文不声称任何代码被运行过也不会给出「实测结果如下」之类的表述。一、执行模型解释执行加动态类型解释执行CPython 先把源码编译成字节码bytecode然后由一个循环常被称作「求值循环」evaluation loop逐条取字节码、分派、执行。对比之下编译型语言在编译期就把很多东西确定下来运行时几乎没有这层「取指令—判断指令—跳转执行」的开销。Python 每条字节码都要走一遍这层分派累加起来就是可观的开销。动态类型Python 的变量没有类型值对象才有类型。a b这一句解释器在运行时才去看a是什么、b是什么、它们的__add__该怎么走。这就是「类型分派」。同时Python 里连整数都是对象它有一个头部装着引用计数、类型指针。你做一个整数运算往往不只是做一次机器加法还牵涉到对象的创建、引用计数的增减——这就是常说的「装箱」boxing成本。小的整数在 CPython 里会被缓存复用大致是 -5 到 256 这个区间但这是实现细节不要依赖。需要特别提醒以上是「机制解释」不要把它换算成「Python 比 C 慢多少倍」。那种数字要看具体任务、具体实现本文不给任何没出处的倍数都不可信。引用计数与内存CPython 用引用计数为主来管理内存配合分代垃圾回收处理循环引用。引用计数的增减是常量时间操作但每次对象赋值、传参都会触发这也是执行模型的一部分成本。二、GIL 与并发的边界GIL 是「全局解释器锁」。它的作用是在同一个进程内同一时刻只有一个线程在执行 Python 字节码。这带来两个直接结论对 CPU 密集任务多线程不会带来并行加速。因为同一时刻只有一个线程在跑字节码多开线程只是轮流执行还要付出切换成本。对 I/O 密集任务多线程是有效的。线程在等待 I/O读写文件、网络请求时会释放 GIL别的线程可以趁机执行。所以网络爬取、文件批处理这类以等待为主的任务多线程能提高吞吐。CPU 密集任务的正确路径是多进程multiprocessing每个进程有独立的解释器和独立的 GIL可以真正并行或者把热点下沉到 C 扩展、原生扩展去实现。关于「Python 已经没有 GIL 了」这个说法不准确。CPython 从 3.13 起提供了实验性的自由线程free-threaded可禁用 GIL构建对应 PEP 703但官方明确说明它是实验性的、默认不启用需要用单独的构建可执行文件名通常带t后缀来运行。默认构建依然有 GIL。任务类型多线程多进程异步 I/O等待网络 / 文件有效可用但偏重有效大量纯计算无明显加速有效能并行不适用混合型视比例而定视比例而定视比例而定三、优化方案按层面下手优化的第一原则是先测量再优化用标准库的timeit模块量出瓶颈在哪而不是凭感觉改。# 适用于 Python 3.8import timeit# timeit(stmt, setup, number)把 stmt 执行 number 次返回总秒数setup data list(range(10000))stmt sum(data)print(timeit.timeit(stmt, setupsetup, number1000))需要比较不同重复次数下的稳定性时用timeit.repeat它返回一串结果可以看波动。1. 算法与数据结构先问数据结构选对了没有。成员判断用列表是线性扫描用集合或字典是哈希查找在数据量大时通常更划算# 适用于 Python 3.8data [i * 2 for i in range(10000)]target 9998# 列表逐个比较in_list target in data# 集合哈希查找seen set(data)in_set target in seenprint(in_list, in_set)两种写法结果相同但复杂度不同。具体快多少取决于数据规模和分布请自己用timeit测。2. 用内置函数和 C 实现替代手写循环sum、max、min、any、all、sorted这些内置函数以及str.join、map、filter底层是用 C 实现的它们把循环藏在 C 里跑省掉了大量字节码分派。# 适用于 Python 3.8nums [3, 1, 4, 1, 5]# 手写循环total 0for n in nums:total n# 内置实现total2 sum(nums)print(total, total2)字符串拼接尤其要注意反复拼接会不断产生新字符串对象而join一次性算出结果。# 适用于 Python 3.8parts [str(i) for i in range(1000)]# 逐次拼接s1 for p in parts:s1 p# 一次性拼接s2 .join(parts)print(len(s1) len(s2))3. 减少重复计算把循环里不变的计算提到循环外是收益最直接的一类优化。函数调用同样如此在循环里反复调re.compile去编译同一个正则就是典型的重复计算。# 适用于 Python 3.8import relines [a1, b22, c333]pattern re.compile(r\d) # 编译一次for line in lines:m pattern.search(line)# 说明re 模块内部会缓存最近编译过的模式# 但显式编译一次再复用语义更清楚也免去缓存查找。用functools.lru_cache做记忆化可以避免对相同参数重复计算# 适用于 Python 3.8from functools import lru_cachelru_cache(maxsizeNone)def fib(n):return n if n 2 else fib(n - 1) fib(n - 2)print(fib(30))lru_cache的参数签名是lru_cache(maxsize128, typedFalse)也可以直接当无参装饰器用在函数上。它的缓存是线程安全的。适合的情况是「相同参数会被反复调用」如果参数每次都不同缓存只会白占内存。4. 局部变量绑定函数里的局部变量访问走的是「按索引取」这条较快路径全局名和属性访问则要多几步查找。把频繁使用的全局名比如某个模块里的函数绑定成局部名可以减少这部分开销。# 适用于 Python 3.8import mathdef calc(values):sqrt math.sqrt # 绑定为局部名return [sqrt(v) for v in values]print(calc([1, 4, 9]))5. 生成器替代一次性大列表列表会把所有元素一次性放进内存生成器则按需产出。当数据只需遍历一次、且体量很大时生成器能显著降低内存峰值。# 适用于 Python 3.8def count_up(n):i 0while i n:yield ii 1total sum(count_up(5)) # 逐个产出不构造整张列表print(total)注意生成器只能遍历一次遍历完就空了。6.__slots__减少每实例开销默认情况下每个实例都带一个__dict__来存属性。用__slots__显式声明属性名后实例不再需要那个字典占用更小属性访问路径也更直接。# 适用于 Python 3.8class Point:__slots__ (x, y)def __init__(self, x, y):self.x xself.y yp Point(1, 2)print(p.x, p.y)代价是不能再给实例动态添加未声明的属性且继承自没有__slots__的类时效果会被削弱。7.array与memoryview需要大量同类型数值时array模块提供更紧凑的存储memoryview允许在不复制底层缓冲区的前提下按视图访问数据。# 适用于 Python 3.8from array import arraynums array(i, range(10)) # 类型码 i 表示有符号整数print(len(nums), nums[3])mv memoryview(bhello)print(mv[0]) # 直接读底层字节不复制常见坑点1. 不做测量就动手优化。❌ 凭直觉改代码改完发现瓶颈根本不在那。✅ 先用timeit量出热点再针对热点改。2. 以为多线程能让计算变快。❌ 给一个纯计算的循环套上多线程期望加速。✅ 受 GIL 限制CPU 密集要用多进程或原生扩展I/O 密集才适合多线程。3. 说「Python 已经没有 GIL 了」。❌ 把 3.13 的实验性自由线程构建当成默认能力。✅ 它默认不启用需单独构建默认解释器仍有 GIL。4. 在循环里反复re.compile。❌ 每次迭代都编译一遍同一个正则。✅ 编译一次、循环外复用。5. 用在循环里拼大量字符串。❌ 逐次拼接反复产生新字符串对象。✅ 收集到列表里用str.join一次性拼。6. 用列表做大量成员判断。❌ 对一个大列表反复做x in lst。✅ 改用具哈希查找的集合或字典。7. 给参数每次都不同的函数加缓存。❌ 无脑上lru_cache缓存全不命中只白占内存。✅ 只在「相同参数会被反复调用」时使用。8. 忽视生成器只能遍历一次。❌ 把一个生成器遍历两遍第二遍得到空结果。✅ 需要多次遍历就物化成列表或每次重新创建生成器。总结层面手段原理执行模型理解解释执行与动态类型每次操作有分派与装箱开销并发多进程 / I/O 用线程GIL 限定了线程内的并行数据结构列表换集合 / 字典哈希查找替代线性扫描循环用内置函数与join把循环下沉到 C 实现重复计算提到循环外、lru_cache同样的结果不重复算内存生成器、__slots__、array减少对象与字典开销「Python 慢」的根源在执行模型和动态类型而不是「解释器写得烂」优化的顺序永远是先测量、再动手选对数据结构往往比微调写法更有效并发要认清 GIL 的边界CPU 密集用多进程I/O 密集才用线程。至于能快多少请在你的数据上用timeit自己得出结论。