定义扩展模块

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 找到。

通常,初始化函数返回一个使用 PyModuleDef_Init() 初始化的模块定义。这允许将创建过程分解为几个阶段

  • 在执行任何实质性代码之前,Python 可以确定模块支持哪些功能,并且可以调整环境或拒绝加载不兼容的扩展。

  • 默认情况下,Python 本身创建模块对象——也就是说,它执行与类的 object.__new__() 等效的操作。它还设置了初始属性,例如 __package____loader__

  • 之后,模块对象使用扩展特定代码进行初始化——等同于类的 __init__()

这称为 *多阶段初始化*,以区别于遗留(但仍受支持)的 *单阶段初始化* 方案,其中初始化函数返回一个完全构建的模块。有关详细信息,请参阅下面的单阶段初始化部分

3.5 版本更改: 增加了对多阶段初始化 (PEP 489) 的支持。

多个模块实例

默认情况下,扩展模块不是单例。例如,如果移除了 sys.modules 条目并重新导入模块,则会创建一个新的模块对象,并且通常会填充新的方法和类型对象。旧模块将进行正常的垃圾回收。这反映了纯 Python 模块的行为。

可以在 子解释器 中或在 Python 运行时重新初始化 (Py_Finalize()Py_Initialize()) 之后创建其他模块实例。在这些情况下,在模块实例之间共享 Python 对象可能会导致崩溃或未定义行为。

为避免此类问题,扩展模块的每个实例都应 *隔离*:对一个实例的更改不应隐式影响其他实例,并且模块拥有的所有状态,包括对 Python 对象的引用,都应特定于特定的模块实例。有关详细信息和实用指南,请参阅隔离扩展模块

避免这些问题的一种更简单的方法是在重复初始化时引发错误

所有模块都应支持子解释器,否则应明确表示不支持。这通常通过隔离或阻止重复初始化来实现,如上所述。模块也可以使用 Py_mod_multiple_interpreters 槽限制在主解释器中。

初始化函数

扩展模块定义的初始化函数具有以下签名

PyObject *PyInit_modulename(void)

其名称应为 PyInit_<name>,其中 <name> 替换为模块的名称。

对于仅包含 ASCII 名称的模块,函数必须命名为 PyInit_<name>,其中 <name> 替换为模块的名称。使用多阶段初始化时,允许使用非 ASCII 模块名称。在这种情况下,初始化函数名称为 PyInitU_<name>,其中 <name> 使用 Python 的 punycode 编码进行编码,并将连字符替换为下划线。在 Python 中

def initfunc_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyInit' + suffix

建议使用辅助宏定义初始化函数

PyMODINIT_FUNC

声明一个扩展模块初始化函数。此宏

  • 指定 PyObject* 返回类型,

  • 添加平台所需的任何特殊链接声明,以及

  • 对于 C++,将函数声明为 extern "C"

例如,名为 spam 的模块将定义如下

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    ...
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

通过定义多个初始化函数,可以从单个共享库导出多个模块。但是,导入它们需要使用符号链接或自定义导入器,因为默认情况下只找到与文件名对应的函数。有关详细信息,请参阅 PEP 489 中的单个库中的多个模块部分。

初始化函数通常是模块 C 源文件中定义的唯一非 static 项。

多阶段初始化

通常,初始化函数 (PyInit_modulename) 返回一个 PyModuleDef 实例,其 m_slots 不为 NULL。在返回之前,必须使用以下函数初始化 PyModuleDef 实例

PyObject *PyModuleDef_Init(PyModuleDef *def)
自 3.5 版本以来成为 稳定 ABI 的一部分。

确保模块定义是正确初始化的 Python 对象,可以正确报告其类型和引用计数。

返回强制转换为 PyObject*def,如果发生错误则返回 NULL

对于多阶段初始化,必须调用此函数。它不应在其他上下文中使用。

请注意,Python 假定 PyModuleDef 结构是静态分配的。此函数可能会返回新引用或借用引用;此引用不得释放。

在 3.5 版本加入。

遗留的单阶段初始化

注意

单阶段初始化是初始化扩展模块的遗留机制,具有已知的缺点和设计缺陷。鼓励扩展模块作者改用多阶段初始化。

在单阶段初始化中,初始化函数 (PyInit_modulename) 应创建、填充并返回一个模块对象。这通常使用 PyModule_Create() 和诸如 PyModule_AddObjectRef() 等函数来完成。

单阶段初始化与默认初始化方式不同,具体如下:

  • 单阶段模块是,或者说 *包含*,“单例”。

    当模块首次初始化时,Python 会保存模块 __dict__ 的内容(即通常是模块的函数和类型)。

    对于随后的导入,Python 不会再次调用初始化函数。相反,它会创建一个新的模块对象,其中包含一个新的 __dict__,并将保存的内容复制到其中。例如,给定一个定义函数 sum 和异常类 error 的单阶段模块 _testsinglephase [1]

    >>> 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 实现细节。

  • 为了解决 PyInit_modulename 不接受 spec 参数的事实,导入机制的某些状态被保存并应用于在 PyInit_modulename 调用期间创建的第一个合适的模块。具体来说,当导入子模块时,此机制会将父包名添加到模块名之前。

    一个单阶段的 PyInit_modulename 函数应尽快创建“其”模块对象,在任何其他模块对象可以创建之前。

  • 不支持非 ASCII 模块名称 (PyInitU_modulename)。

  • 单阶段模块支持模块查找函数,例如 PyState_FindModule()