Python 的 import 到底做了什么?首次 2 毫秒与缓存 1 微秒,中间隔着一个 sys.modules
结论先行:同一个模块,首次 import 花 2048 微秒,第二次只要 1.1 微秒——差了约 1800 倍。中间隔着的不是魔法,是 sys.modules 这个全局缓存。所以"函数里写 import 会不会很慢"这类担心基本是多余的:实测函数内重复 import 10 万次,每次只要 193 纳秒。本文用实机数据把 import 的完整流程拆一遍,顺带讲清 __pycache__ 和循环导入这两个高频坑。
1. 先看差距:2048µs vs 1.1µs
import sys, time
del sys.modules['json'] # 强制清掉缓存
t0 = time.perf_counter_ns(); import json; t1 = time.perf_counter_ns()
print((t1-t0)/1000, 'us') # 2048.6 us ≈ 2 毫秒
t0 = time.perf_counter_ns(); import json; t1 = time.perf_counter_ns()
print(t1-t0, 'ns') # 1100 ns ≈ 1.1 微秒
实测(CPython 3.13.14):首次 import json 花 2048.6µs,第二次 1100ns,差约 1800 倍。首次的钱花在哪?找文件、读源码、编译成字节码、执行模块顶层代码、注册缓存——一整套流程;第二次只干一件事:去 sys.modules 字典里查个 key。
2. sys.modules:import 的第一站,也是唯一的缓存
sys.modules 是一个普通字典,key 是模块名,value 是模块对象。解释器启动后这里已经躺了一百多个模块:
import sys
print(len(sys.modules)) # 我这台机器上是 107
print('json' in sys.modules) # import 之后 True
任何 import x,第一步永远是 sys.modules 里查 x。查到了就直接返回引用,后面的事全省了。这就是为什么第二次 import 只要 1 微秒——一次字典查找的成本。理解这一点,就理解了 import 性能的 90%。
3. 首次 import 的完整流水线(查不到缓存才走这里)
缓存 miss 之后,CPython 按顺序做这几件事:
- 找:遍历
sys.meta_path里的 finder(内置模块、冻结模块、路径查找器),路径查找器再按sys.path逐个目录翻; - 读:找到
.py文件后读源码; - 编译:源码编译成字节码(这步最贵,毫秒级);
- 缓存字节码:默认把编译结果写进
__pycache__/xxx.cpython-313.pyc,下次跳过第 3 步; - 执行:新建模块对象,执行模块顶层代码(顶层语句只在这一步跑一次);
- 注册:挂进
sys.modules,之后的 import 全部命中缓存。
拿一个真实模块实测(200 个函数定义的 .py,本测试环境关闭了字节码缓存,正好暴露纯编译成本):
# 首次:读源码 + 编译 + 执行 → 6153.2µs
# 清掉 sys.modules 后再 import:3288.0µs(没有 .pyc 可省,只能重编译)
默认环境下第二次磁盘级 import 会读 __pycache__ 里的 .pyc,省掉编译那一步。 我这个沙箱把 sys.dont_write_bytecode 设成了 True,.pyc 没写出来,所以两次都得重新编译——这组"反常"数据反而把 .pyc 缓存的价值衬托得很清楚:有它,省的是毫秒级的编译;没它,每次启动都要付一遍。
4. 函数里写 import 到底贵不贵?193 纳秒
很多人说"import 要写在文件顶部,函数里写会慢"。实测一下:
def f():
import json
return json
import timeit
print(timeit.timeit("f()", globals={'f': f}, number=100000)) # 0.019s
函数内 import 10 万次,总共 19ms,每次 193 纳秒。 因为 json 早就在 sys.modules 里,函数内的 import 就是一次字典查找加一次局部变量绑定。真正要注意的不是性能,而是另外两件事:循环导入和** import 时机副作用**(顶层代码只在首次执行一次,靠函数内 import 延迟触发是常见技巧)。
5. 循环导入:唯一会把 import 玩坏的场景
# a.py
import b
X = 'a'
# b.py
import a # a 还没执行完,sys.modules 里的 a 是"半成品"
print(a.X) # AttributeError
原理用本文的机制一句话说清:import a 时,a 先被塞进 sys.modules 再执行顶层代码;执行到一半 b 又去 import a,此时缓存里虽有 a,但它的顶层代码还没跑完,X 还不存在——拿到的是一个"半成品"模块。解法就三条:把 import 下沉到函数里(利用 193ns 的廉价缓存查找)、抽公共模块、或者改用 import a 而非 from a import X(推迟到用的时候才取属性)。
6. 对照小结(收藏这张表)
| 操作 | 实测耗时 | 背后动作 |
|---|---|---|
| 首次 import json | 2048.6µs | 找文件+读+编译+执行+注册 |
| 缓存命中 import | 1100ns | sys.modules 查字典,快约 1800 倍 |
| 函数内重复 import | 193ns/次 | 同上,可放心写 |
| 无 .pyc 时重编译(200 函数模块) | 3288µs | .pyc 缓存被禁用的代价 |
import a vs from a import X |
查找相同 | 后者多一次属性绑定,且扛循环导入更差 |
小结:import 的性能账,本质是"一次完整流水线 + 之后全是字典查找"。sys.modules 管内存级缓存(微秒级),__pycache__ 管磁盘级缓存(省编译)。函数内 import 不可怕,循环导入才是真要命的那个。
你项目里写过函数内的 import 吗?是 deliberate 的延迟加载,还是被循环导入逼的?评论区聊聊。
实测环境:CPython 3.13.14(64 位 Windows)。注:本测试环境
sys.dont_write_bytecode=True,故 .pyc 相关数字反映的是"无字节码缓存"场景,与默认环境的差异已在文中标注。

浙公网安备 33010602011771号