首页
/ CPython C API 模块导入体系:PyImport_* 函数族完全实战指南

CPython C API 模块导入体系:PyImport_* 函数族完全实战指南

2026-09-04 10:10:11作者:韦蓉瑛

本篇指南基于 CPython 官方文档 Importing Modules 展开,系统讲解 C 扩展与嵌入式应用如何通过 PyImport_* 系列函数完成模块导入、模块对象注册、字节码装载、冻结模块处理和内置模块注册等核心操作。读完后,你将能够:在 C 代码中正确调用各导入 API 并处理返回值与异常、理解每个函数在导入管线中的位置与参考计数(strong/borrowed reference)差异、结合 Python/import.cInclude/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 referenceborrowed 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_ImportModuleExInclude/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 形式,但要注意两个限制(文档原文强调):

  1. 该函数不加载、不导入模块——若模块此前未导入,你拿到的是一个空模块对象;要真正导入请使用 PyImport_ImportModule 及其变体;
  2. 点号名称隐含的包结构(父包对象链)不会被创建。

三个变体的区别仅在参数类型与参考计数:

函数 参数类型 返回引用 引入版本
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 并设置异常。文档给出了四个重要语义点:

  1. 失败即清理:出错时 name 会从 sys.modules 中移除——即使调用前它已在表里。文档解释了动机:"把未初始化完的模块留在 sys.modules 里是危险的,因为后续导入方无从得知该模块处于作者意图之外的损坏状态。" 如果调用方想恢复原有模块对象(比如重载失败的场景),文档建议参考 PyImport_ReloadModule 的做法(重载前先保存旧引用,失败再放回)。
  2. __spec__ / __loader__ 会被补全:若尚未设置,模块的 __spec____loader__ 会被设置为合适值;spec 的 loader 取模块的 __loader__(若已设置),否则为 importlib.machinery.SourceFileLoader 的实例。
  3. __file__ 设置:模块的 __file__ 设为代码对象的 co_filename(基线版本;见下文的 pathname 变体)。
  4. 重复调用即重载:若模块已导入,该函数会重新加载它;但"有意的重载"应使用 PyImport_ReloadModule
  5. 点号名称隐含的包结构同样不会被创建。

版本变化值得注意: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),非 NULLpathname 会覆盖模块的 __file__
  • PyImport_ExecCodeModuleObject(name, co, pathname, cpathname):全 PyObject* 版本(3.3+),文档明确说"三者中优先使用这个",cpathname(编译后文件路径,即 .pyc 路径)会在非 NULL 时被"恰当地使用";
  • PyImport_ExecCodeModuleWithPathnames(3.2+):全 UTF-8 字符串版本,并多做一件事——pathnameNULLcpathnameNULL 时,尝试从 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 的实现可以看到几处细节:SetLazyImportsFilterPy_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 使用。

常见陷阱与选型速查

综合文档说明与源码证据,嵌入式/扩展开发中建议遵循以下规则:

  1. 参考计数:只有 PyImport_AddModuleRef 返回强引用;AddModule / AddModuleObject 是借用引用,DECREF 借用引用是典型崩溃源。其余返回 PyObject* 的导入函数(ImportImportModuleExecCodeModule*ReloadModuleImportModuleAttr*CreateModuleFromInitfunc 等)均返回新(强)引用。
  2. inittab 时序AppendInittab / ExtendInittab 必须在每次 Py_Initialize 之前调用,事后调用是 Py_FatalError 级致命错误;数组必须带 NULL 哨兵。
  3. 重载选型:有意重载用 PyImport_ReloadModule(即 importlib.reload);PyImport_ExecCodeModule 系列虽"顺带"能重载,但文档明确那是非预期用法。
  4. __file__ 覆盖:需要控制模块 __file__ 时用 ExecCodeModuleEx / ExecCodeModuleObject / WithPathnames,三者中 ExecCodeModuleObject 是文档推荐的"首选"。
  5. 版本边界AddModuleRef 需 3.13+;ImportModuleAttr* 需 3.14+;惰性导入四件套需 3.15+;ExecCodeModule* 自 3.12 起弃用 __cached__/__loader__ 设置、3.15 起彻底不设置 __cached__——跨版本代码不要依赖这些副作用。
  6. 缓存标签取数:写 .pyc 路径逻辑时以 sys.implementation.cache_tag 为准,PyImport_GetMagicTag 只是同源的内部快捷方式。

以上全部行为描述均可在仓库中逐条复核:API 声明见 Include/import.hInclude/cpython/import.h,实现见 Python/import.cPyImport_Import 在 L4689 附近、PyImport_ReloadModule 在 L4658、inittab 管理在 L2620 附近、ExecCodeModule 家族在 L2771 附近、惰性导入 API 在 L4985 附近),冻结模块运行期逻辑见 Python/frozen.c,冻结模块生成工具见 Programs/_freeze_module.pyPython/frozen_modules/。官方文档原文位于 Doc/c-api/import.rst

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384