本节目标:拆开
import语句背后的三段式协议——find_spec查找、create_module建模块、exec_module执行——并说清__spec__/__loader__/__file__这些属性究竟由谁写入。
适用版本:Python 3.12+(实测 3.14.6)
7.1 导入协议与 finder / loader
import foo 看起来是一个原子操作,但 CPython 内部把它拆成「找、建、跑」三步,每一步都由一个可替换的对象负责。理解这三步之后,自定义导入器、惰性加载、模块热重载都会变得顺理成章。
一、一条名为 _find_and_load 的流水线
真正执行 import 的代码不在 importlib 的 Python 文件里,而在冻结模块 importlib._bootstrap 中(sys.modules["importlib._bootstrap"])。核心函数 _find_and_load_unlocked 的顺序如下:
- 先查
sys.modules,命中就直接返回缓存对象(不重新执行); - 遍历
sys.meta_path上的 finder,对每个调用find_spec(name, path, target),第一个返回非None的ModuleSpec胜出; - 用
spec.loader.create_module(spec)造出模块对象(返回None则用默认模块); - 在
exec_module之前,把模块对象放进sys.modules[name]; - 调用
spec.loader.exec_module(module)执行模块顶层代码; - 返回模块对象。
第 4 步的先后顺序是循环导入报错的根源,也是 importlib.reload 语义的关键,本节末尾会实测。
二、sys.meta_path:三个默认 finder
打印 sys.meta_path,会看到三个类(不是实例),分属两个冻结模块:
import sys
for f in sys.meta_path:
print(f.__module__ + "." + f.__name__)
_frozen_importlib.BuiltinImporter
_frozen_importlib.FrozenImporter
_frozen_importlib_external.PathFinder
它们的分工是:
| finder | 负责 | origin 取值 |
|---|---|---|
BuiltinImporter | sys、builtins 等编译进解释器的模块 | 'built-in' |
FrozenImporter | 冻结模块(importlib._bootstrap、os、abc 等) | 'frozen' |
PathFinder | 磁盘上的 .py/.pyc/扩展模块,即绝大多数模块 | 文件路径 |
前两个是「编译期就存在」的模块,查找是 O(1) 的名字匹配;只有 PathFinder 才真的去翻文件系统。往 sys.meta_path 头部插入自己的 finder,就能在任何磁盘查找之前拦截导入——7.2 会用到这一点。
三、PathFinder 与 FileFinder
PathFinder 自己不做文件匹配,它把活外包给 FileFinder:遍历 sys.path 的每一项,对每个目录用 sys.path_hooks 里的钩子试出一个「路径入口 finder」(path entry finder),再让它去找子模块。默认钩子只有两个:
import sys
print([getattr(h, "__name__", h) for h in sys.path_hooks])
['zipimporter', 'path_hook_for_FileFinder']
FileFinder 实例内部持有一张「后缀 → loader」表,决定了同一个模块名会优先匹配哪种文件:
import sys, importlib.machinery as m
import site
sp = next(p for p in sys.path if p.endswith("site-packages"))
finder = sys.path_importer_cache.get(sp)
for suffix, loader in finder._loaders:
print(repr(suffix), "->", loader.__name__)
'.cpython-314-darwin.so' -> ExtensionFileLoader
'.abi3.so' -> ExtensionFileLoader
'.so' -> ExtensionFileLoader
'.py' -> SourceFileLoader
'.pyc' -> SourcelessFileLoader
注意 .cpython-314-darwin.so 排在 .so 之前:同名扩展模块若同时存在带 ABI 标记与不带标记的版本,CPython 会优先选前者。查找到的 finder 会按目录缓存在 sys.path_importer_cache 里,所以同一目录不会反复触发钩子。
四、ModuleSpec:一次查找的全部结果
finder 的产出是一个 ModuleSpec,它是「怎么加载这个模块」的完整描述。打印标准库 json 的 spec:
import json
print(json.__spec__)
ModuleSpec(name='json',
loader=<_frozen_importlib_external.SourceFileLoader object at 0x...>,
origin='.../lib/python3.14/json/__init__.py',
submodule_search_locations=['.../lib/python3.14/json'])
几个字段的含义:
| 字段 | 含义 |
|---|---|
name | 模块全名(json、json.decoder) |
loader | 负责建模块与执行的 loader |
origin | 来源;文件路径、'built-in'、'frozen' 或 None |
submodule_search_locations | 非空表示这是包,值是 __path__ 的来源 |
cached | .pyc 缓存路径(可写) |
has_location | 是否给模块设置 __file__ |
parent | 父包名,写入 __package__ |
json 是包,所以 submodule_search_locations 非空、cached 指向 __pycache__/__init__.cpython-314.pyc。而内置模块 sys 的 spec 里 origin='built-in'、loader 是 BuiltinImporter 类本身,且没有 __file__。
五、三阶段协议实测
loader 协议只有两个方法。写一个最小 loader,把三个阶段打印出来:
import sys
from importlib.abc import MetaPathFinder, Loader
from importlib.machinery import ModuleSpec
SOURCE = "value = 7\nprint('[mod] executing top-level code')\n"
class TinyFinder(MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == "tinymod":
print(" 1. find_spec -> ModuleSpec")
return ModuleSpec(fullname, TinyLoader())
return None
class TinyLoader(Loader):
def create_module(self, spec):
print(" 2. create_module -> None (use default module)")
return None
def exec_module(self, module):
print(" 3. exec_module")
exec(compile(SOURCE, "<tinymod>", "exec"), module.__dict__)
sys.meta_path.insert(0, TinyFinder())
import tinymod
print("tinymod.value =", tinymod.value)
1. find_spec -> ModuleSpec
2. create_module -> None (use default module)
3. exec_module
[mod] executing top-level code
tinymod.value = 7
create_module 返回 None 是约定:让导入机制造一个普通的 types.ModuleType。返回自定义对象则可以实现「模块对象本身是别的类型」——例如让 import 直接给出一个 dict 或数据库连接。
六、spec_from_file_location:手动加载
不用 import 语句,也能从任意路径加载一个 .py。这正是插件系统、配置加载器的底层写法:
import importlib.util, sys, os
path = os.path.join(os.getcwd(), "manual", "greeting.py")
spec = importlib.util.spec_from_file_location("greeting", path)
mod = importlib.util.module_from_spec(spec)
sys.modules["greeting"] = mod # 想支持相对导入/可 pickle 时必须先注册
spec.loader.exec_module(mod)
print(mod.MARK, "|", mod.__file__)
loaded-by-hand | /private/tmp/.../manual/greeting.py
两个容易被忽略的点:其一,module_from_spec 不会把模块放进 sys.modules,必须手动注册,否则包内相对导入与 pickle 都会失败;其二,模块对象在 exec_module 之前就已经有了 __file__,因为它是 _init_module_attrs 依据 spec.has_location 提前写入的。
七、__spec__ / __loader__ / __file__ 从哪来
这些属性不是模块代码自己写的,而是导入机制在 exec_module 之前统一注入:
| 模块属性 | 来源 |
|---|---|
__name__ | spec.name |
__spec__ | 这个 ModuleSpec 对象本身 |
__loader__ | spec.loader |
__package__ | spec.parent(顶层模块为 '') |
__file__ | spec.origin,仅当 has_location 为真 |
__cached__ | spec.cached |
对 json 实测:json.__spec__.loader is json.__loader__ 为真,json.__file__ 等于 spec.origin。而 sys 没有 __file__,因为内置模块的 has_location 为假。
八、sys.modules 的缓存命中时序
用一个会在执行期自检的模块,观察「模块何时进入缓存」:
# sideeffect.py
import sys
print("during exec, in sys.modules:", "sideeffect" in sys.modules)
print("partially built:", hasattr(sys.modules.get("sideeffect"), "VALUE"))
VALUE = 42
during exec, in sys.modules: True
partially built: False
模块在 exec_module 进行中已经在 sys.modules 里,但 VALUE 尚未绑定——这就是「部分初始化的模块」。随后:
- 第二次
import sideeffect直接返回缓存对象,不再打印任何执行日志; del sys.modules["sideeffect"]后再import,会重新执行顶层代码,得到一个新的模块对象。
这也解释了循环导入:A 执行到一半去导入 B,B 回头 from A import x,此时 sys.modules["A"] 存在但 x 还没定义,于是抛 ImportError: cannot import name 'x' from partially initialized module 'A'。
九、版本分界:3.12 删掉了旧协议
在 3.11 之前,finder 还允许实现 find_module(),loader 还允许实现 load_module()。这些方法在 3.12 被移除(importlib.abc.Finder、module_repr() 支持一并删除)。因此在 3.12+ 上:
- finder 只需实现
find_spec(); - loader 只需实现
create_module()与exec_module(); - 若还在写
find_module(),它不会被调用,只会静默失效。
小结
import是一条「查sys.modules→ 遍历sys.meta_path→find_spec→create_module→ 注册进缓存 →exec_module」的流水线。- 默认
sys.meta_path是BuiltinImporter、FrozenImporter、PathFinder三个类;只有PathFinder会访问文件系统。 ModuleSpec承载一次查找的全部信息,submodule_search_locations非空即为包,has_location决定是否写__file__。__spec__/__loader__/__file__/__package__都由导入机制在exec_module之前注入,不是模块自己声明的。- 模块在
exec_module执行前就已进入sys.modules,这正是循环导入报错与「部分初始化」现象的来源。 - 3.12 起旧协议
find_module()/load_module()已被移除,只保留find_spec()与create_module()/exec_module()。
下一节 元路径钩子与自定义导入器
会把本节的三阶段协议用起来:往 sys.meta_path 插入自己的 finder,拦截并改写导入行为,再配合 .pth、sitecustomize 与 importlib.reload 讲清钩子与缓存的完整图景。
阅读导航:上一节:结构化并发与调试 · 下一节:元路径钩子与自定义导入器 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。