定义扩展模块

针对 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 模块的行为。modules.

额外的模块实例可能会在 子解释器 中或者 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 中的 Multiple modules in one library 一节了解详情。

初始化函数通常是定义于模块的 C 源代码中唯一的非 static 条目。

多阶段初始化

通常,初始化函数 (PyInit_modulename) 返回一个带有非 NULLm_slotsPyModuleDef 实例。 在它被返回之前,这个 PyModuleDef 实例必须先使用以下函数来初始化:instance must be initialized using the following function:

PyObject *PyModuleDef_Init(PyModuleDef *def)
属于 稳定 ABI 自 3.5 版起.

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

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

调用此函数是 多阶段初始化 所必需的。 它不应在其他上下文中被使用。

请注意 Python 会假定 PyModuleDef 结构体是静态分配的。 此函数可以返回一个新引用或借入引用;这个引用不可被释放。

Added in version 3.5.

旧式的单阶段初始化

注意

单阶段初始化是一种用于初始化扩展模块的旧机制,它具有已知的缺点和设计瑕疵。 建议扩展模块作者改用多阶段初始化。

在单阶段初始化中,初始化函数 (PyInit_modulename) 应当完成模块对象的创建、填充和返回操作。通常使用 PyModule_Create()PyModule_AddObjectRef() 等函数实现该流程。

单阶段初始化与 默认方式 的主要区别如下:

  • 单阶段模块本质上是(更准确地说,包含)"单例对象"。

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

    对于后续导入操作,Python不会再次调用初始化函数,而是创建一个带有新 __dict__ 的模块对象,并将已保存的内容复制到其中。例如,假设有一个单阶段模块 _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的实现细节。

  • 为解决 PyInit_modulename 函数不接受 spec 参数的限制,导入机制会保存部分状态,并在 PyInit_modulename 调用期间将其应用于首个匹配的模块对象。具体表现为:当导入子模块时,该机制会将父包名称自动前置到模块名前。

    单阶段 PyInit_modulename 函数应当尽早创建"其所属"模块对象,该操作需在任何其他模块对象创建之前完成。

  • 不支持非ASCII模块命名格式(如 PyInitU_modulename)。

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