《Python高级编程》7.1 导入协议与 finder / loader

从 CPython 实现层拆开 import:sys.meta_path 上三个默认 finder 的查找顺序、PathFinder/FileFinder 的路径搜索、find_spec/create_module/exec_module 三阶段协议,并用实测说清 __spec__/__loader__/__file__ 的来源与 sys.modules 的缓存时序。

本节目标:拆开 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 的顺序如下:

  1. 先查 sys.modules,命中就直接返回缓存对象(不重新执行);
  2. 遍历 sys.meta_path 上的 finder,对每个调用 find_spec(name, path, target),第一个返回非 None 的 ModuleSpec 胜出;
  3. 用 spec.loader.create_module(spec) 造出模块对象(返回 None 则用默认模块);
  4. 在 exec_module 之前,把模块对象放进 sys.modules[name];
  5. 调用 spec.loader.exec_module(module) 执行模块顶层代码;
  6. 返回模块对象。

第 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 取值
BuiltinImportersys、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(),它不会被调用,只会静默失效。

小结

  1. import 是一条「查 sys.modules → 遍历 sys.meta_path → find_spec → create_module → 注册进缓存 → exec_module」的流水线。
  2. 默认 sys.meta_path 是 BuiltinImporter、FrozenImporter、PathFinder 三个类;只有 PathFinder 会访问文件系统。
  3. ModuleSpec 承载一次查找的全部信息,submodule_search_locations 非空即为包,has_location 决定是否写 __file__。
  4. __spec__/__loader__/__file__/__package__ 都由导入机制在 exec_module 之前注入,不是模块自己声明的。
  5. 模块在 exec_module 执行前就已进入 sys.modules,这正是循环导入报错与「部分初始化」现象的来源。
  6. 3.12 起旧协议 find_module()/load_module() 已被移除,只保留 find_spec() 与 create_module()/exec_module()。

下一节 元路径钩子与自定义导入器 会把本节的三阶段协议用起来:往 sys.meta_path 插入自己的 finder,拦截并改写导入行为,再配合 .pth、sitecustomize 与 importlib.reload 讲清钩子与缓存的完整图景。

阅读导航:上一节:结构化并发与调试 · 下一节:元路径钩子与自定义导入器 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时