定义扩展模块¶
针对 CPython 的 C 扩展就是一个共享库(例如,Linux 上的 .so 文件,Windows 上的 .pyd DLL),它可被加载到 Python 进程中(例如,它可以用兼容的编译器设置进行编译),而这会导出一个 导出钩子 函数 (或旧式的 初始化函数)。
要在默认情况下可被导入 (也就是说,通过 importlib.machinery.ExtensionFileLoader),共享库必须在 sys.path 中可用,并且必须命名为模块名之后加一个在 importlib.machinery.EXTENSION_SUFFIXES 中列出的扩展名。
备注
构建、打包和分发扩展模块最好使用第三方工具完成,并且超出了本文的范围。一个合适的工具是 Setuptools,其文档可以在 https://setuptools.pypa.io/en/latest/setuptools.html 上找到。
扩展导出钩子¶
在 3.15 版本加入: 对 PyModExport_<name> 导出钩子的支持是在 Python 3.15 中增加的。 定义模块的旧方式仍然可用:如果你计划支持更早的 Python 版本可以查阅 PyInit 函数 一节或本文档的较早版本。
导出钩子必须是具有以下签名的导出函数:
对于具有仅限 ASCII 名称的模块,导出钩子 必须被命名为 PyModExport_<name>,其中 <name> 以模块名称替换。
对于非 ASCII 模块名称,则导出钩子必须被命名为 PyModExportU_<name> (注意增加了 U),其中 <name> 使用 Python 的 punycode 编码格式进行编码并将连字符替换为下划线。 在 Python 中:
def hook_name(name):
try:
suffix = b'_' + name.encode('ascii')
except UnicodeEncodeError:
suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
return b'PyModExport' + suffix
导出钩子将返回由 PySlot 条目组成的数组,以一个槽位 ID 为 0 的条目作为终结。 这些槽位描述了模块应当如何被创建和初始化。
这个数组必须保持可用与恒定直到解释器关闭。 通常,它应当使用 static 存储。 建议针对任何动态行为使用 Py_mod_create 和 Py_mod_exec 槽位。
导出钩子可以返回 NULL 并设置一个异常来发出失败信号。
建议使用一个辅助宏来定义导出钩子函数:
-
PyMODEXPORT_FUNC¶
- This macro is available when compiling for the 稳定 ABI 自 3.15 版起.
声明一个扩展模块导出钩子。 这个宏:
指定 PySlot* 返回类型,
添加平台所需的任何特殊链接声明,以及
对于 C++,将函数声明为
extern "C"。
例如,一个名为 spam 的模块可以这样定义:
PyABIInfo_VAR(abi_info);
static PySlot spam_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "spam"),
PySlot_FUNC(Py_mod_exec, spam_init_function),
...
PySlot_END
};
PyMODEXPORT_FUNC
PyModExport_spam(void)
{
return spam_slots;
}
导出钩子通常是定义于模块 C 源代码中唯一的非 static 条目。
此钩子应当保持简短。 如果它在 return 一个静态数组之个还做了其他事,则需注意几点:
如果你需要使用任何 Python C API,在常见的 ABI 不匹配情况下,建议先调用
PyABIInfo_Check()来引发异常,而不是让程序崩溃。导出钩子中的代码绝不可依赖于 GIL: Python 的 自由线程构建版 只能在钩子返回后检查
Py_mod_gil槽位(或确定其不存在)。类似地,此钩子可在任意子解释器中被调用,因为对
Py_mod_multiple_interpreters槽位的存在(或不存在)的检查只在钩子返回后才执行。
例如:
PyMODEXPORT_FUNC
PyModExport_modulename(void)
{
if (PyABIInfo_Check(&abi_info, "modulename") < 0) {
/* ABI mismatch. It's not safe to examine the raised exception. */
return NULL;
}
/* use Python API (as little as possible); don't rely on GIL */
return modulename_slots;
}
备注
通过定义多个导出钩子,可以从同一个共享库中导出多个模块。然而,导入这些模块需要自定义导入器,或具备合适名称的扩展文件副本/链接,因为 Python 的导入机制只会找到与文件名相对应的函数。详情请参阅 PEP 489 中的 Multiple modules in one library 部分。
多阶段初始化¶
创建扩展模块的过程包含以下几个阶段:
Python 会查找并调用导出钩子,以获取有关如何创建该模块的信息。
在执行任何实质性代码之前,Python 可以确定该模块支持哪些功能,并可以调整环境或拒绝加载不兼容的扩展。诸如
Py_mod_abi、Py_mod_gil和Py_mod_multiple_interpreters之类的槽位会影响这一步骤。默认情况下,Python 随后会创建模块对象——也就是说,它在创建对象时相当于调用
__new__()。这一步可以通过Py_mod_create槽位进行覆盖。Python 会为模块设置诸如
__package__和__loader__之类的初始属性,并将模块对象插入到sys.modules中。随后,模块对象会以一种与扩展相关的特定方式完成初始化——这相当于创建对象时执行
__init__(),或相当于在 Python 语言模块中执行顶层代码。其行为通过Py_mod_exec槽位进行定义。
这被称为*多阶段初始化*,用以将其与传统的(但仍受支持的) single-phase initialization 区分开来;在后者中,初始化函数会返回一个已完全构建好的模块。
在 3.5 版本发生变更: 增加了对多阶段初始化的支持 (PEP 489)。
多个模块实例¶
默认情况下,扩展模块并非单例。例如,若移除 sys.modules 条目并重新导入该模块,则会创建一个新的模块对象;通常,该对象会被填充全新的方法和类型对象。旧模块则遵循常规的垃圾回收机制。这与纯 Python 模块的行为一致。
额外的模块实例可能会在 子解释器 中或者 Python 运行时重新初始化之后 (Py_Finalize() 和 Py_Initialize()) 被创建。在这些情况下,模块实例间共享 Python 对象可能导致程序崩溃或未定义的行为。
为避免这种问题,每个扩展模块的实例都应当是 隔离的: 对一个实例的修改不应隐式地影响其他的实例,以及模块所拥有的全部状态,包括对 Python 对象的引用,都应当是特定模块实例专属的。请参阅 隔离扩展模块 了解更多的细节和实用的指南。
一个避免这些问题的简单方式是 针对重复的初始化引发一个错误。
所有模块都应当支持 子解释器,否则就要显式地发出缺乏支持的信号。 这往往是通过隔离或阻止重复的初始化,如上文所述。一个模块也可以使用 Py_mod_multiple_interpreters 槽位将其限制于主解释器中。
PyInit 函数¶
自 3.15 版起已处于 Soft deprecated 状态: 该功能不会获得新功能,但目前暂无将其移除的计划。
与 PyModExport_modulename() 不同,扩展模块可以定义一种采用旧式签名的 initialization function,其签名如下:
其名称应为 PyInit_<name>,其中 <name> 应替换为模块名称。对于非 ASCII 模块名称,则应使用 PyInitU_<name>;此时 <name> 的编码方式应与 export hook 相同(即使用带下划线的 Punycode)。
如果某个模块同时导出 PyInit_<name> 和 PyModExport_<name>,则 PyInit_<name> 函数将被忽略。
与 PyMODEXPORT_FUNC 类似,建议使用辅助宏来定义初始化函数:
通常,初始化函数 (PyInit_modulename) 会返回一个包含非 NULL m_slots 的 PyModuleDef 实例。这使得 Python 能够使用 multi-phase initialization。
在将其返回之前,必须使用以下函数对 PyModuleDef 实例进行初始化:
-
PyObject *PyModuleDef_Init(PyModuleDef *def)¶
- 属于 稳定 ABI 自 3.5 版起.
确保模块定义是一个正确初始化的 Python 对象,并正确报告其类型以及引用计数。
返回强制转换为
PyObject*的 def,或者如果出现错误则返回NULL。在模块初始化函数中返回
PyModuleDef之前,必须调用此函数。不应在其他上下文中使用此函数。请注意 Python 会假定
PyModuleDef结构体是静态分配的。此函数可以返回一个新引用或借用引用;这个引用不可被释放。在 3.5 版本加入.
例如,一个名为 spam 的模块可以这样定义:
static struct PyModuleDef spam_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "spam",
...
};
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModuleDef_Init(&spam_module);
}
旧式的单阶段初始化¶
自 3.15 版起已处于 Soft deprecated 状态: 单阶段初始化是一种用于初始化扩展模块的旧机制,它具有已知的缺点和设计瑕疵。建议扩展模块作者改用多阶段初始化。
不过,目前暂无计划取消对其的支持。
在单阶段初始化过程中,旧式 initialization function (PyInit_modulename) 应创建、填充并返回一个模块对象。通常通过 PyModule_Create() 以及 PyModule_AddObjectRef() 等函数来实现这一操作。
单阶段初始化与 默认方式 的主要区别如下:
将
PyModuleDef.m_size设置为 -1 的单相模块,或者更准确地说,是 包含 “单例”的模块。当此类模块首次完成初始化时,Python 会保存该模块的
__dict__内容(通常即该模块中的函数和类型)。对于后续的导入操作,Python 不会再次调用初始化函数;相反,它会创建一个带有新
__dict__的新模块对象,并将已保存的内容复制到其中。模块通过 (module_name,file_name) 元组与已保存的内容进行匹配,其中:module_name 是该模块的完整点分隔名称,取自
ModuleSpec.name(或导入函数中对应的参数)。请注意,初始化函数用于创建模块的名称(例如PyModuleDef.m_name)不会被纳入考虑。file_name 是扩展名对应的文件名。对于未从文件中加载的模块(例如内置模块,以及通过
PyImport_CreateModuleFromInitfunc()或PyImport_AppendInittab()添加的模块),该变量未设置。
作为首次初始化的一部分,Python 还会以该模块的名称将其添加到
sys.modules中。例如,假设有一个单相位模块_testsinglephase[1],该模块定义了一个函数sum和一个异常类error:>>> import sys >>> import _testsinglephase as one >>> del sys.modules['_testsinglephase'] >>> import _testsinglephase as two >>> one is two False >>> one.__dict__ is two.__dict__ False >>> one.sum is two.sum True >>> one.error is two.error True
该具体行为应被视为 CPython 的实现细节。
对于具有非负
PyModuleDef.m_size的单相模块,Python 可能会像对待m_size == -1那样,在 (module_name,file_name) 键下缓存一个指向初始化函数的指针。后续指定不同初始化函数的导入(例如使用
PyImport_CreateModuleFromInitfunc())可能会调用缓存函数,也可能调用所提供的“新”函数。具体行为应再次被视为 CPython 的实现细节。
为解决
PyInit_modulename函数不接受 spec 参数的限制,导入机制会保存部分状态,并在PyInit_modulename调用期间将其应用于首个匹配的模块对象。具体表现为:当导入子模块时,该机制会将父包名称自动前置到模块名前。单阶段
PyInit_modulename函数应当尽早创建"其所属"模块对象,该操作需在任何其他模块对象创建之前完成。不支持非 ASCII 模块名 (
PyInitU_modulename)。单阶段模块支持模块查找函数如
PyState_FindModule()。该模块的
PyModuleDef.m_slots必须为 NULL。