CPython C API 模块导入体系:PyImport_* 函数族完全实战指南
本篇指南基于 CPython 官方文档 Importing Modules 展开,系统讲解 C 扩展与嵌入式应用如何通过 PyImport_* 系列函数完成模块导入、模块对象注册、字节码装载、冻结模块处理和内置模块注册等核心操作。读完后,你将能够:在 C 代码中正确调用各导入 API 并处理返回值与异常、理解每个函数在导入管线中的位置与参考计数(strong/borrowed reference)差异、结合 Python/import.c 与 Include/import.h 的源码核实各函数的底层行为,从而在扩展模块或嵌入式解释器中安全地操作 Python 导入机制。
PyImport 函数族总览
CPython 将模块导入的 C API 集中在 Include/import.h 中声明,实现主体位于 Python/import.c(约 5700 行)。文档定义了以下函数族,按用途可分为五类:
| 函数 | 用途 | 关键点 |
|---|---|---|
PyImport_ImportModule |
按名称(UTF-8 字符串)导入模块 | PyImport_Import 的字符串包装 |
PyImport_ImportModuleEx |
带 globals/locals/fromlist 导入 | 实际是宏,等价于 level=0 的 Level 调用 |
PyImport_ImportModuleLevelObject |
完整复刻 __import__ 语义(含相对导入) |
3.3 起;标准 __import__ 直接调用它 |
PyImport_ImportModuleLevel |
同上,但名称为 UTF-8 字符串 | 3.3 起负数 level 不再被接受 |
PyImport_Import |
高层导入,走当前环境的导入钩子 | 始终为绝对导入 |
PyImport_ReloadModule |
重载已导入模块 | 等价于 importlib.reload |
PyImport_AddModuleRef |
按名称取/建模块对象 | 3.13 起;返回强引用 |
PyImport_AddModuleObject |
同上,名称为 str 对象 |
3.3 起;返回借用引用 |
PyImport_AddModule |
同上,名称为 UTF-8 字符串 | 返回借用引用 |
PyImport_ExecCodeModule 系列 |
从代码对象加载模块 | 失败时从 sys.modules 移除模块 |
PyImport_GetMagicNumber / PyImport_GetMagicTag |
字节码魔数与缓存标签 | 3.3 起失败返回 -1 |
PyImport_GetModuleDict / PyImport_GetModule |
访问 sys.modules |
按解释器隔离 |
PyImport_GetImporter |
为 sys.path 条目取 finder |
走 sys.path_hooks 并缓存 |
PyImport_ImportFrozenModule 系列 |
加载冻结(frozen)模块 | 返回 int:1/0/-1 |
PyImport_AppendInittab / PyImport_ExtendInittab |
注册内置模块 | 必须在 Py_Initialize 之前 |
PyImport_ImportModuleAttr 系列 |
导入模块并取属性 | 3.14 起 |
PyImport_SetLazyImportsMode 系列 |
控制惰性导入(lazy import) | 3.15 起 |
PyImport_CreateModuleFromInitfunc |
从 spec + 初始化函数创建模块 | 3.15 起;自定义静态扩展导入器用 |
参考计数是这组 API 最大的易错点。文档中反复出现的 strong reference 与 borrowed reference 意味着:PyImport_AddModuleRef 成功后调用方必须 Py_DECREF,而 PyImport_AddModule / PyImport_AddModuleObject 返回的引用不允许释放。选择哪个函数取决于你之后是否需要持有该模块对象的独立所有权。
执行导入:PyImport_Import 与 import 语义
PyImport_Import:走当前环境的导入钩子
PyImport_Import(PyObject *name) 是"高层接口":它调用当前环境安装好的"导入钩子函数",即以显式 level=0(绝对导入)调用当前全局命名空间 __builtins__ 里的 __import__ 函数。文档同时强调:该函数始终使用绝对导入。
从 Python/import.c 的实现可以核实这条描述:PyImport_Import 先通过 PyEval_GetGlobals() 取当前帧的 globals,再从其中取出 __builtins__,然后从 __builtins__ 里取出 __import__ 并调用;如果当前线程没有 globals,则构造一个带标准 builtins 的临时字典兜底。这意味着如果你在解释器中 monkey-patch 了 builtins.__import__(例如实现虚拟文件系统或自定义导入机制),C 端调用 PyImport_Import 也会走到你的钩子——这正是文档所说"导入由当前环境安装的导入钩子执行"的底层依据。
实现里还有一个值得注意的细节:源码注释说明传入的 fromlist 是一个空列表占位(注释中举 win32com.client.gencache 的例子,指出会返回 gencache 模块而不是 win32com)。也就是说 PyImport_Import("a.b.c") 的行为与 import a.b.c 后取 a.b.c 一致,而不是返回顶层包。
PyImport_ImportModule / Ex / Level 系列
PyImport_ImportModule(const char *name) 是 PyImport_Import 的字符串版本包装(入参从 PyObject* 换成 const char*)。
PyImport_ImportModuleEx 在 Include/import.h 中直接定义为宏:
#define PyImport_ImportModuleEx(n, g, l, f) \
PyImport_ImportModuleLevel((n), (g), (l), (f), 0)
即固定 level=0 转发给 PyImport_ImportModuleLevel。因此"Ex"只是历史命名,行为与 level=0 的 Level 调用完全一致。
PyImport_ImportModuleLevel(及其对象版 PyImport_ImportModuleLevelObject,3.3 引入)完整复刻内置函数 __import__ 的语义——文档明确指出标准的 __import__ 就是直接调用 LevelObject 版本。返回值规则与 __import__ 相同:
- 成功时返回对已导入模块或顶层包的新引用(强引用);
- 请求子模块(如
a.b.c)时,默认返回的是顶层包a,除非传入了非空fromlist; - 失败时返回
NULL且已设置异常; - 导入失败时,不完整的模块对象会从
sys.modules中移除(与PyImport_ImportModule行为一致)。
level 参数决定相对导入的深度(对应 from . import x 中的点号数量)。文档标注了一个版本变化:自 3.3 起,负的 level 值不再被接受。
模块对象的注册、查询与重载
AddModule 三兄弟:查表或建空模块
PyImport_AddModule(const char *name) 返回模块名对应的模块对象。其行为是:先看 sys.modules 字典里有没有,没有就新建一个空模块对象并插入字典。名称可以是 package.module 形式,但要注意两个限制(文档原文强调):
- 该函数不加载、不导入模块——若模块此前未导入,你拿到的是一个空模块对象;要真正导入请使用
PyImport_ImportModule及其变体; - 点号名称隐含的包结构(父包对象链)不会被创建。
三个变体的区别仅在参数类型与参考计数:
| 函数 | 参数类型 | 返回引用 | 引入版本 |
|---|---|---|---|
PyImport_AddModuleRef |
const char*(UTF-8) |
强引用 | 3.13 |
PyImport_AddModuleObject |
PyObject*(str) |
借用引用 | 3.3 |
PyImport_AddModule |
const char*(UTF-8) |
借用引用 | 早期 |
从 Include/import.h 的头文件声明可以看到,AddModuleRef 需要 Py_LIMITED_API >= 0x030d0000(即 3.13+)才可见,AddModuleObject 需要 3.3+。如果你依赖稳定 ABI(limited API)编译扩展,PyImport_AddModuleRef 只在较新的限制 ABI 下可用。
GetModuleDict / GetModule:直接操作 sys.modules
PyImport_GetModuleDict() 返回模块管理字典(即 sys.modules),文档提醒这是按解释器隔离的变量(per-interpreter variable)——在多解释器场景中拿到的是当前解释器的那份表。
PyImport_GetModule(PyObject *name)(3.7 引入)只查不建:返回已经导入的模块;未导入时返回 NULL 且不设置错误(区别于查询失败时的 NULL + 异常)。这使得它能安全地用于"模块是否存在"探测而不清除既有异常状态。
PyImport_ReloadModule:等价 importlib.reload
PyImport_ReloadModule(PyObject *m) 重载模块,成功返回重载后模块的新引用,失败返回 NULL 并设置异常(此时模块本身仍存在于 sys.modules)。Python/import.c 的实现非常直白:先取得 importlib 模块(查表失败则尝试导入),然后调用 importlib.reload(m)。从源码结构看,这意味着 C API 重载与 Python 侧 importlib.reload 共享全部语义,包括对无法重载模块类型的处理。
PyImport_ImportModuleAttr(3.14 新增)
PyImport_ImportModuleAttr(mod_name, attr_name) 与 PyImport_ImportModuleAttrString 是"导入 + 取属性"的组合助手,参数分别为 Python str 对象或 UTF-8 字符串。从 Python/import.c 可见实现就是 PyImport_Import 后接 PyObject_GetAttr 再释放模块引用。异常语义随之分层:模块不存在抛 ImportError,属性不存在抛 AttributeError。
从代码对象装载模块:PyImport_ExecCodeModule 家族
这组函数用于"已经拿到代码对象"的场景(例如从 .pyc 文件读入、或刚用 compile() 生成),是自定义加载器、字节码打包工具的关键 API。
核心语义(以 PyImport_ExecCodeModule 为基线)
给定模块名(可为 package.module)和代码对象 co,执行并返回模块对象的新引用;出错返回 NULL 并设置异常。文档给出了四个重要语义点:
- 失败即清理:出错时
name会从sys.modules中移除——即使调用前它已在表里。文档解释了动机:"把未初始化完的模块留在sys.modules里是危险的,因为后续导入方无从得知该模块处于作者意图之外的损坏状态。" 如果调用方想恢复原有模块对象(比如重载失败的场景),文档建议参考PyImport_ReloadModule的做法(重载前先保存旧引用,失败再放回)。 __spec__/__loader__会被补全:若尚未设置,模块的__spec__与__loader__会被设置为合适值;spec 的 loader 取模块的__loader__(若已设置),否则为importlib.machinery.SourceFileLoader的实例。__file__设置:模块的__file__设为代码对象的co_filename(基线版本;见下文的 pathname 变体)。- 重复调用即重载:若模块已导入,该函数会重新加载它;但"有意的重载"应使用
PyImport_ReloadModule。 - 点号名称隐含的包结构同样不会被创建。
版本变化值得注意:3.12 起对 __cached__ 和 __loader__ 的设置被弃用(文档建议改用 importlib.machinery.ModuleSpec 的对应机制);3.15 起 __cached__ 属性不再被设置。跨版本编写的加载器不要依赖这些副作用。
三个变体的调用链与 pathname 推导
Python/import.c 显示整个家族最终汇聚到一个实现:
PyObject *
PyImport_ExecCodeModule(const char *name, PyObject *co)
{
return PyImport_ExecCodeModuleWithPathnames(
name, co, (char *)NULL, (char *)NULL);
}
PyImport_ExecCodeModuleEx(name, co, pathname):等价于WithPathnames(name, co, pathname, NULL),非NULL的pathname会覆盖模块的__file__;PyImport_ExecCodeModuleObject(name, co, pathname, cpathname):全PyObject*版本(3.3+),文档明确说"三者中优先使用这个",cpathname(编译后文件路径,即.pyc路径)会在非NULL时被"恰当地使用";PyImport_ExecCodeModuleWithPathnames(3.2+):全 UTF-8 字符串版本,并多做一件事——当pathname为NULL而cpathname非NULL时,尝试从cpathname反推源码路径。从源码看(Python/import.c),反推通过调用importlib._bootstrap_external的_get_sourcefile完成,这替代了 3.12 已移除的imp.source_from_cache。
字节码魔数与缓存标签
PyImport_GetMagicNumber() 返回 Python 字节码文件(.pyc)的魔数。文档说明:魔数应位于字节码文件前四个字节,小端字节序;3.3 起出错返回 -1(此前无明确的错误返回值约定)。实现上(Python/import.c)它直接返回编译期常量 PYC_MAGIC_NUMBER_TOKEN。
PyImport_GetMagicTag()(3.2+)返回 PEP 3147 格式字节码文件名的魔数标签字符串(如 cpython-313 这类标签的一部分)。文档特别提醒:sys.implementation.cache_tag 的值才是权威来源,应优先使用它而非此函数。源码实现印证了这一点:PyImport_GetMagicTag 只是返回内部变量 _PySys_ImplCacheTag,与 sys.implementation.cache_tag 同源。
查找器机制:PyImport_GetImporter
PyImport_GetImporter(PyObject *path) 针对 sys.path(或包的 __path__)中的单个路径条目 path,返回对应的finder 对象:
- 先查
sys.path_importer_cache缓存字典; - 未命中时依次遍历
sys.path_hooks,直到找到一个能处理该路径的钩子; - 没有任何钩子能处理时返回
None——文档解释"这告知调用方:基于路径的查找器无法为该路径条目找到 finder"; - 结果会写回
sys.path_importer_cache缓存; - 成功时返回 finder 对象的新引用。
这是 C 层自定义 finder/loader 体系时的入口:你不需要自己重复 sys.path_hooks 的遍历逻辑,直接拿 finder 再走 find_spec 即可。
冻结模块(Frozen Modules)
冻结模块指在构建期把字节码打进可执行文件、运行期直接从内存导入的模块。文档定义了支撑这一机制的结构与常量。
struct _frozen 与 PyImport_FrozenModules
_frozen 结构定义(文档标注见 Include/import.h;在当前源码树中该结构实际位于 Include/cpython/import.h):
struct _frozen {
const char *name; /* ASCII 编码的模块名 */
const unsigned char *code; /* 字节码 */
int size;
int is_package; /* 3.11 起:是否为包 */
};
版本变化:3.11 引入 is_package 字段,取代了此前"把 size 置为负值表示包"的旧约定。
PyImport_FrozenModules 是指向 _frozen 记录的数组指针,以全 NULL/零项结尾;导入冻结模块时在此表中查找。文档还指出一个嵌入式技巧:"第三方代码可以耍点花样(play tricks)替换这个指针,从而提供动态创建的冻结模块集合。"
PyImport_ImportFrozenModule 系列
PyImport_ImportFrozenModule(const char *name) 与对象版 PyImport_ImportFrozenModuleObject(3.3+)加载冻结模块,返回值语义是 int 三态:
1:成功;0:模块不存在;-1:初始化失败且已设置异常。
成功后要用 PyImport_ImportModule 才能拿到模块对象。文档用"misnomer"(名不副实)一词提醒:若模块已被导入,此函数会重载它。另外 3.4 起不再为冻结模块设置 __file__ 属性。
仓库中的冻结工具链
CPython 源码树自带完整的冻结基础设施:冻结模块的生成入口是 Programs/_freeze_module.py(配套 C 端 Programs/_freeze_module.c),生成物存放在 Python/frozen_modules/ 目录,运行期的装载逻辑在 Python/frozen.c。阅读这些文件可以对照上文 _frozen 结构,看到 is_package 标志在生成代码中的实际写法。
注册内置模块:_inittab 表与初始化函数
嵌入 Python 的应用可以通过 inittab 机制声明自己的内置模块。
结构定义
_inittab 结构(Include/cpython/import.h)只有两个成员:
struct _inittab {
const char *name; /* ASCII 编码字符串 */
PyObject* (*initfunc)(void);
};
文档特别注明:inittab 使用的是 PyInit_xxx 风格的初始化函数;目前无法通过 inittab 使用 PyModExport_ 导出钩子。
两个注册函数及其硬性约束
PyImport_AppendInittab(name, initfunc):便捷包装,向表追加单条记录,表扩容失败返回-1。新模块以 name 导入,首次尝试导入时调用 initfunc。PyImport_ExtendInittab(struct _inittab *newtab):批量追加;newtab数组必须以 name 为 NULL 的哨兵项结尾——文档警告缺少哨兵"可能导致内存故障(memory fault)"。成功返回0,内存不足返回-1且不添加任何模块。若 Python 被多次初始化,每次初始化前都要重新调用这两个函数。
约束的强制程度可以从 Python/import.c 的实现看到:两个函数开头都检查 INITTAB != NULL,若在 Py_Initialize() 之后调用,直接触发 Py_FatalError("...may not be called after Py_Initialize()") 终止进程——这不是温和的异常,而是致命错误。PyImport_ExtendInittab 的实现还展示了内存策略:用 _PyMem_DefaultRawRealloc 强制使用默认原始分配器合并新旧表,目的是在解释器析构时能确定地释放这块内存。
PyImport_Inittab 变量本身就是内置模块表,文档明确"不要直接使用",一律走上面两个函数。从 Include/cpython/import.h 的注释还能看到它的生命周期:"This is not used after Py_Initialize() is called"——初始化后内部另有独立副本(即 INITTAB)。
PyImport_CreateModuleFromInitfunc(3.15 新增)
PyImport_CreateModuleFromInitfunc(PyObject *spec, PyObject* (*initfunc)(void)) 是 3.15 引入的构建块,面向自定义静态扩展导入器(例如导入静态链接扩展的场景),对应 importlib.abc.Loader.create_module 步骤:
- spec 必须是
importlib.machinery.ModuleSpec对象; - initfunc 的约束与
PyImport_AppendInittab相同; - 成功时创建并返回模块对象,但该模块尚未初始化——调用方须调用
PyModule_Exec完成初始化(自定义导入器应在其exec_module方法里做这件事); - 失败返回
NULL并设置异常。
从 Python/import.c 的实现看,它校验 initfunc 非空、从 spec 取 name 属性且必须是字符串,然后调用内部 create_builtin 完成创建。Include/cpython/import.h 的注释也概括了它的定位:让自定义导入器"直接从 spec 和初始化函数初始化静态链接扩展模块,而不必走 inittab"。
惰性导入控制(3.15 新增 API 组)
CPython 3.15 引入了一组控制 lazy import 的 API,让嵌入方可以全局调节源码中 lazy 关键字的行为。
PyImport_LazyImportsMode 枚举
typedef enum {
PyImport_LAZY_NORMAL, /* 默认:尊重源码中的 lazy 关键字 */
PyImport_LAZY_ALL /* 让所有导入默认变为惰性 */
} PyImport_LazyImportsMode;
四个控制函数
PyImport_GetLazyImportsMode():返回当前模式;PyImport_SetLazyImportsMode(mode):设置模式,文档声明"此函数总是返回 0";PyImport_GetLazyImportsFilter():返回当前过滤器的强引用,无过滤器时返回NULL,"此函数总是成功";PyImport_SetLazyImportsFilter(filter):设置过滤器,成功返回0,失败返回-1并设置异常。
过滤器契约(文档原文语义):filter 必须是一个可调用对象,当一次导入可能变为惰性时被调用,参数为 (importing_module_name, imported_module_name, [fromlist]);其中 imported_module_name 是解析后的模块名——例如 lazy from .spam import eggs 会传入 package.spam(而非 .spam);可调用对象返回 True 表示该导入应变为惰性,False 则否。
从 Python/import.c 的实现可以看到几处细节:SetLazyImportsFilter 把 Py_None 归一化为 NULL(即传 None 可清除过滤器);非可调用对象会抛 ValueError("filter provided but is not callable");过滤器以强引用保存在解释器状态(interp->imports.lazy_imports_filter)中,且替换过程持有专用锁(源码注释解释了为何不能用 Py_XSETREF:需要先在持锁状态下交换指针,再释放锁后对旧引用做 decref)。模式值则通过原子操作存储在同一解释器状态里,保证多线程可见性。这些 API 在 Include/import.h 中位于 #ifndef Py_LIMITED_API 保护内,即仅限非限制 API 使用。
常见陷阱与选型速查
综合文档说明与源码证据,嵌入式/扩展开发中建议遵循以下规则:
- 参考计数:只有
PyImport_AddModuleRef返回强引用;AddModule/AddModuleObject是借用引用,DECREF借用引用是典型崩溃源。其余返回PyObject*的导入函数(Import、ImportModule、ExecCodeModule*、ReloadModule、ImportModuleAttr*、CreateModuleFromInitfunc等)均返回新(强)引用。 - inittab 时序:
AppendInittab/ExtendInittab必须在每次Py_Initialize之前调用,事后调用是Py_FatalError级致命错误;数组必须带 NULL 哨兵。 - 重载选型:有意重载用
PyImport_ReloadModule(即importlib.reload);PyImport_ExecCodeModule系列虽"顺带"能重载,但文档明确那是非预期用法。 __file__覆盖:需要控制模块__file__时用ExecCodeModuleEx/ExecCodeModuleObject/WithPathnames,三者中ExecCodeModuleObject是文档推荐的"首选"。- 版本边界:
AddModuleRef需 3.13+;ImportModuleAttr*需 3.14+;惰性导入四件套需 3.15+;ExecCodeModule*自 3.12 起弃用__cached__/__loader__设置、3.15 起彻底不设置__cached__——跨版本代码不要依赖这些副作用。 - 缓存标签取数:写
.pyc路径逻辑时以sys.implementation.cache_tag为准,PyImport_GetMagicTag只是同源的内部快捷方式。
以上全部行为描述均可在仓库中逐条复核:API 声明见 Include/import.h 与 Include/cpython/import.h,实现见 Python/import.c(PyImport_Import 在 L4689 附近、PyImport_ReloadModule 在 L4658、inittab 管理在 L2620 附近、ExecCodeModule 家族在 L2771 附近、惰性导入 API 在 L4985 附近),冻结模块运行期逻辑见 Python/frozen.c,冻结模块生成工具见 Programs/_freeze_module.py 与 Python/frozen_modules/。官方文档原文位于 Doc/c-api/import.rst。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00