Python 的 import 为什么第二次几乎不要钱?sys.modules 缓存与模块单例的代价
结论先行:import 的「第二次免费」靠的是一张字典——sys.modules。模块一旦加载过,再次 import 只是 230 纳秒的字典查询;但第一次 import 要走过「查找→编译→执行」全链路,实测一个 1500 个函数的模块冷启动 46.8 毫秒,有 pyc 缓存后降到 3.9 毫秒。这篇用 8 组实验讲清缓存机制、模块单例、pyc 到底省了什么,以及循环 import 为什么有时能跑有时崩。—## 1. 第二次 import 只是一次字典查询python import sys, timet0 = time.perf_counter()for _ in range(100000): import jsonprint((time.perf_counter() – t0) / 100000) # 2.3e-07 秒实测 10 万次 import json 总共 23 毫秒——平均每次 230 纳秒。这不是「重新加载了一遍 json」,而是 import 系统第一件事就是查 sys.modules 字典,命中就直接返回。import 语句的编译结果里甚至没有文件 IO,只有一个字典查找加名字绑定。
2. sys.modules:整个 import 系统的缓存表sys.modules 是一个普通的 dict,键是模块名,值是模块对象。它的行为决定了 import 的三个特点:其一,同进程同名单例。 import json as j2 之后 j2 is json 恒为 True——全进程只有一个 json 模块对象,所有 import 者共享同一份状态。这是模块级单例模式的实现基础,也是「修改模块属性会影响所有使用者」的原因。
其二,import 是一棵树。 import json 实际加载的是 json 全家:```python>>> import decimal, sys>>> sorted(k for k in sys.modules if k == ‘json’ or k.startswith(‘json.’))[‘json’, ‘json.decoder’, ‘json.encoder’, ‘json.scanner’]
‘json’ in sys.modules # import decimal 之后True # decimal 把 json 也拖进来了```一次 import 触发的是整棵依赖树的加载,树上的每个节点都进 sys.modules。这也是「import 开销」难以预估的原因:你以为 import 了一个小工具,它可能拖进来半个标准库。## 3. 打破单例的两种方式,代价完全不同模块对象进了 sys.modules 就稳如泰山,但有两把刀能动它:
| 操作 | 模块对象 | 旧引用 | 适用场景 ||—|—|—|—|| importlib.reload(m) | 复用同一对象,就地重执行代码 | 自动看到新代码 | 交互式调试 || del sys.modules['m'] 后重新 import | 创建全新对象 | 仍指向旧对象,成「孤儿」 | 测试隔离 |实测确认:reload 前后 id(m) 相同——它把模块代码在原有命名空间里重新跑一遍,所以旧引用无需更新就能看到新定义;而删字典条目再 import,拿到的是另一个对象,旧引用还挂在老对象上,两个副本的状态会分叉。测试里 mock 模块用后者,线上热更新想都别想。
4. pyc 省的是编译,不是执行sys.modules 管的是「进程内」,pyc 管的是「进程间」。第一次 import 一个 1500 个函数的模块(源码 65KB):| 场景 | 耗时 | 说明 ||—|—|—|| 冷启动(无 pyc) | 46.8 ms | 读源码→编译字节码→执行模块体 |
| 热启动(有 pyc) | 3.9 ms | 跳过编译,直接执行模块体 |快了 12 倍,但剩下的 3.9 毫秒一分没少——pyc 只省「源码→字节码」的编译,模块体的执行(定义 1500 个函数、建 1500 个对象)每次 import 都要重跑。这解释了一个常见困惑:明明有 __pycache__,为什么 import 还是慢——**编译可以缓存,执行不能。**顺带一提:PYTHONDONTWRITEBYTECODE=1 的环境(比如多数 Docker 镜像)连 pyc 都不写,每次冷启动全额付费。
5. importtime:把 import 开销摊开看CPython 自带 -X importtime,直接看每笔账:textimport time: 3391 | 15806 | decimalimport time: 4063 | 9423 | _decimalimport time: 4807 | 57830 | json读法:第一列是模块自身耗时(μs),第二列是含子模块的累计。decimal 自身只要 3.4 毫秒,但连带 _decimal 和 json,累计 15.8 毫秒。做启动优化时,先跑 importtime 找累计值最大的那几个,比猜快得多。
6. 循环 import:为什么有时能跑,有时崩python# cyc_a.pyimport cyc_b # 此时 cyc_b 开始执行VAL_A = 1# cyc_b.pyimport cyc_a # cyc_a 已在 sys.modules(部分初始化)→ 拿到半成品VAL_B = 2
这套能跑:import cyc_b 拿到的是 sys.modules 里已注册但只执行了一半的 cyc_a 模块对象,只要 cyc_b 不在模块顶层立刻访问 cyc_a.VAL_A,相安无事。但把 import cyc_b 换成 from cyc_b import VAL_B,如果 cyc_b 又在顶层 from cyc_a import VAL_A,直接崩:textImportError: cannot import name 'VAL_C' from partially initialized module 'cyc_c'规律:import x 形式对循环宽容(拿到半成品也行),from x import y 形式苛刻(必须当场拿到属性)。 所以循环 import 的救命写法就是统一用 import x + 函数内延迟访问。
网硕互联帮助中心



评论前必须登录!
注册