Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 489 – 多阶段扩展模块初始化

作者:
Petr Viktorin <encukou at gmail.com>,Stefan Behnel <stefan_ml at behnel.de>,Alyssa Coghlan <ncoghlan at gmail.com>
BDFL 委托
Eric Snow <ericsnowcurrently at gmail.com>
讨论至:
Import-SIG 邮件列表
状态:
最终版
类型:
标准跟踪
创建日期:
2013年8月11日
Python 版本:
3.5
发布历史:
2013年8月23日,2015年2月20日,2015年4月16日,2015年5月7日,2015年5月18日
决议:
Python-Dev 消息

目录

重要

本 PEP 是一份历史文献。最新的规范文档现在可以在 初始化 C 模块 中找到。对于 Python 3.14+,请参阅 定义扩展模块模块定义

×

有关如何提出更改,请参阅 PEP 1

摘要

本 PEP 提议重新设计内置模块和扩展模块与导入机制交互的方式。这在 Python 3.0 中通过 PEP 3121 进行了最后一次修订,但当时并未解决所有问题。目标是通过使扩展模块更接近 Python 模块的行为方式来解决导入相关问题;特别是要挂接到 PEP 451 中引入的基于 ModuleSpec 的加载机制。

本提案借鉴了 PEP 384PyType_Spec,允许扩展作者只定义他们需要的功能,并允许未来对扩展模块声明进行添加。

扩展模块以两步过程创建,更好地融入 ModuleSpec 架构,类似于类的 __new____init__

扩展模块可以安全地在模块中存储任意 C 级模块状态,这些状态由正常的垃圾回收覆盖,并支持重载和子解释器。鼓励扩展作者在使用新 API 时考虑这些问题。

该提案还允许使用非 ASCII 名称的扩展模块。

PEP 3121 中并非所有问题都在本提案中得到解决。特别是,运行时模块查找 (PyState_FindModule) 的问题留待未来的 PEP 解决。

动机

Python 模块和扩展模块的设置方式不同。对于 Python 模块,首先创建并设置模块对象,然后执行模块代码(PEP 302)。ModuleSpec 对象(PEP 451)用于保存有关模块的信息,并传递给相关钩子。

对于扩展(即共享库)和内置模块,模块初始化函数会立即执行,并同时完成创建和初始化。初始化函数不会传递 ModuleSpec 或其中包含的任何信息,例如 __file__ 或完全限定名称。这阻碍了相对导入和资源加载。

在 Py3 中,模块也不会被添加到 sys.modules,这意味着(可能是传递的)重新导入模块将真正尝试重新导入它,从而在再次执行模块初始化函数时陷入无限循环。如果没有对完全限定模块名称的访问权限,也无法正确地将模块添加到 sys.modules。这对于 Cython 生成的模块来说尤其是一个问题,因为它们的模块初始化代码的复杂性通常与任何“常规”Python 模块相同。此外,缺少 __file____name__ 信息阻碍了“__init__.py”模块(即包)的编译,特别是在模块初始化时使用相对导入时。

此外,当前大多数现有扩展模块在子解释器支持和/或解释器重载方面存在问题,虽然现有基础设施可以支持这些功能,但既不容易也不高效。解决这些问题是 PEP 3121 的目标,但许多扩展(包括标准库中的一些)采取了最省力的方法移植到 Python 3,使这些问题悬而未决。本 PEP 保持向后兼容性,这应该会减轻压力,并为扩展作者提供充足的时间在移植时考虑这些问题。

当前流程

目前,扩展模块和内置模块导出一个名为“PyInit_modulename”的初始化函数,该函数以共享库的文件名命名。此函数由导入机制执行,并且必须返回一个完全初始化的模块对象。该函数不接收任何参数,因此它无法知道其导入上下文。

在执行过程中,模块初始化函数根据 PyModuleDef 对象创建一个模块对象。然后,它通过向模块字典添加属性、创建类型等来继续初始化它。

在后台,共享库加载器会记录它加载的最后一个模块的完全限定模块名称,当创建一个名称匹配的模块时,此全局变量用于确定模块对象的完全限定名称。这并不完全安全,因为它依赖于模块初始化函数首先创建自己的模块对象,但这个假设在实践中通常是成立的。

提案

初始化函数(PyInit_modulename)将被允许返回指向 PyModuleDef 对象的指针。导入机制将负责构造模块对象,在初始化的相关阶段(如下所述)调用 PyModuleDef 中提供的钩子。

这种多阶段初始化是一种额外的可能性。单阶段初始化(即返回完全初始化模块对象的当前实践)仍将被接受,因此现有代码将保持不变,包括二进制兼容性。

PyModuleDef 结构将更改为包含一个槽列表,类似于 PEP 384 的类型 PyType_Spec。为了保持二进制兼容性并避免引入新结构(这将引入额外的支持函数和每个模块存储),PyModuleDef 当前未使用的 m_reload 指针将更改为保存槽。结构定义如下

typedef struct {
    int slot;
    void *value;
} PyModuleDef_Slot;

typedef struct PyModuleDef {
    PyModuleDef_Base m_base;
    const char* m_name;
    const char* m_doc;
    Py_ssize_t m_size;
    PyMethodDef *m_methods;
    PyModuleDef_Slot *m_slots;  /* changed from `inquiry m_reload;` */
    traverseproc m_traverse;
    inquiry m_clear;
    freefunc m_free;
} PyModuleDef;

m_slots 成员必须为 NULL,或指向 PyModuleDef_Slot 结构的数组,以 ID 设置为 0 的槽终止(即 {0, NULL})。

要指定一个槽,必须提供一个唯一的槽 ID。新的 Python 版本可能会引入新的槽 ID,但槽 ID 永远不会被回收。槽可能会被弃用,但将在整个 Python 3.x 版本中继续支持。

槽的值指针不能为 NULL,除非槽的文档中另有规定。

以下槽目前可用,并将在后面描述

  • Py_mod_create
  • Py_mod_exec

未知槽 ID 将导致导入失败并出现 SystemError。

在使用多阶段初始化时,PyModuleDefm_name 字段在导入期间将不被使用;模块名称将取自 ModuleSpec。

在从 PyInit_* 返回之前,PyModuleDef 对象必须使用新添加的 PyModuleDef_Init 函数进行初始化。这会设置对象类型(在某些编译器上无法静态完成)、引用计数和内部簿记数据(m_index)。例如,扩展模块“example”将导出为

static PyModuleDef example_def = {...}

PyMODINIT_FUNC
PyInit_example(void)
{
    return PyModuleDef_Init(&example_def);
}

PyModuleDef 对象必须在其创建的模块的生命周期内可用——通常,它将静态声明。

伪代码概述

以下是修改后的导入器将如何操作的概述。省略了日志记录或错误和无效状态处理等细节,C 代码以简洁的 Python 样语法呈现。

调用导入器的框架在 PEP 451 中进行了解释。

importlib/_bootstrap.py:

class BuiltinImporter:
    def create_module(self, spec):
        module = _imp.create_builtin(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

importlib/_bootstrap_external.py:

class ExtensionFileLoader:
    def create_module(self, spec):
        module = _imp.create_dynamic(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

Python/import.c_imp 模块)

def create_dynamic(spec):
    name = spec.name
    path = spec.origin

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    return _PyImport_LoadDynamicModuleWithSpec(spec)

def exec_dynamic(module):
    if not isinstance(module, types.ModuleType):
        # non-modules are skipped -- PyModule_GetDef fails on them
        return

    def = PyModule_GetDef(module)
    state = PyModule_GetState(module)
    if state is NULL:
        PyModule_ExecDef(module, def)

def create_builtin(spec):
    name = spec.name

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    for initname, initfunc in PyImport_Inittab:
        if name == initname:
            m = initfunc()
            if isinstance(m, PyModuleDef):
                def = m
                return PyModule_FromDefAndSpec(def, spec)
            else:
                # fall back to single-phase initialization
                module = m
                _PyImport_FixupExtensionObject(module, name, name)
                return module

Python/importdl.c:

def _PyImport_LoadDynamicModuleWithSpec(spec):
    path = spec.origin
    package, dot, name = spec.name.rpartition('.')

    # see the "Non-ASCII module names" section for export_hook_name
    hook_name = export_hook_name(name)

    # call platform-specific function for loading exported function
    # from shared library
    exportfunc = _find_shared_funcptr(hook_name, path)

    m = exportfunc()
    if isinstance(m, PyModuleDef):
        def = m
        return PyModule_FromDefAndSpec(def, spec)

    module = m

    # fall back to single-phase initialization
    ....

Objects/moduleobject.c:

def PyModule_FromDefAndSpec(def, spec):
    name = spec.name
    create = None
    for slot, value in def.m_slots:
        if slot == Py_mod_create:
            create = value
    if create:
        m = create(spec, def)
    else:
        m = PyModule_New(name)

    if isinstance(m, types.ModuleType):
        m.md_state = None
        m.md_def = def

    if def.m_methods:
        PyModule_AddFunctions(m, def.m_methods)
    if def.m_doc:
        PyModule_SetDocString(m, def.m_doc)

def PyModule_ExecDef(module, def):
    if isinstance(module, types.module_type):
        if module.md_state is NULL:
            # allocate a block of zeroed-out memory
            module.md_state = _alloc(module.md_size)

    if def.m_slots is NULL:
        return

    for slot, value in def.m_slots:
        if slot == Py_mod_exec:
            value(module)

模块创建阶段

模块对象的创建——即 ExecutionLoader.create_module 的实现——由 Py_mod_create 槽控制。

Py_mod_create 槽

Py_mod_create 槽用于支持自定义模块子类。值指针必须指向具有以下签名的函数

PyObject* (*PyModuleCreateFunction)(PyObject *spec, PyModuleDef *def)

该函数接收一个 ModuleSpec 实例(如 PEP 451 中定义)和 PyModuleDef 结构。它应该返回一个新的模块对象,或者设置一个错误并返回 NULL。

此函数不负责在新模块上设置 PEP 451 中指定的导入相关属性(例如 __name____loader__)。

返回的对象不要求是 types.ModuleType 的实例。只要它支持设置和获取属性(包括至少导入相关属性),任何类型都可以使用。但是,只有 ModuleType 实例支持模块特定功能,例如每个模块状态和执行槽的处理。如果返回的不是 ModuleType 子类,则可能未定义任何执行槽;如果定义了任何执行槽,则会引发 SystemError

请注意,当调用此函数时,模块在 sys.modules 中的条目尚未填充。尝试再次导入相同的模块(可能是传递的)可能会导致无限循环。建议扩展作者将 Py_mod_create 保持最小化,尤其是不要从中调用用户代码。

不能指定多个 Py_mod_create 槽。如果指定了多个,导入将失败并出现 SystemError

如果未指定 Py_mod_create,则导入机制将使用 PyModule_New 创建一个普通模块对象。名称取自 spec

创建后步骤

如果 Py_mod_create 函数返回 types.ModuleType 或其子类的实例(或者如果 Py_mod_create 槽不存在),则导入机制将 PyModuleDef 与模块关联。这也使得 PyModuleDef 可用于执行阶段、PyModule_GetDef 函数和垃圾回收例程(遍历、清除、释放)。

如果 Py_mod_create 函数不返回模块子类,则 m_size 必须为 0,并且 m_traversem_clearm_free 必须都为 NULL。否则,将引发 SystemError

此外,PyModuleDef 中指定的初始属性将设置在模块对象上,无论其类型如何

  • 文档字符串从 m_doc 设置,如果非 NULL。
  • 模块的函数从 m_methods 初始化,如果有的话。

模块执行阶段

模块执行——即 ExecutionLoader.exec_module 的实现——由“执行槽”控制。本 PEP 只添加一个 Py_mod_exec,但将来可能会添加其他槽。

执行阶段在与模块对象关联的 PyModuleDef 上完成。对于不是 PyModule_Type 子类的对象(对于这些对象,PyModule_GetDef 将失败),执行阶段将被跳过。

执行槽可以多次指定,并按它们在槽数组中出现的顺序进行处理。当使用默认导入机制时,它们在 PEP 451 中指定的导入相关属性(例如 __name____loader__)设置后以及模块添加到 sys.modules 后进行处理。

执行前步骤

在处理执行槽之前,为模块分配每个模块状态。从此时起,每个模块状态可以通过 PyModule_GetState 访问。

Py_mod_exec 槽

此槽中的条目必须指向具有以下签名的函数

int (*PyModuleExecFunction)(PyObject* module)

它将被调用以初始化模块。通常,这相当于设置模块的初始属性。“module”参数接收要初始化的模块对象。

函数必须在成功时返回 0,或者在错误时设置异常并返回 -1

如果 PyModuleExec 替换了 sys.modules 中的模块条目,则新对象将被导入库机制使用并在所有执行槽处理后返回。这是导入机制本身的一个功能。槽本身都是使用从创建阶段返回的模块处理的;在执行阶段不咨询 sys.modules。(请注意,对于扩展模块,实现 Py_mod_create 通常是使用自定义模块对象的更好解决方案。)

旧版初始化

向后兼容的单阶段初始化将继续得到支持。在此方案中,PyInit 函数返回一个完全初始化的模块,而不是 PyModuleDef 对象。在这种情况下,PyInit 钩子实现创建阶段,执行阶段为空操作。

需要在旧版本 Python 上不变工作的模块应坚持使用单阶段初始化,因为它带来的好处无法向后移植。以下是一个支持多阶段初始化并在为旧版本 CPython 编译时回退到单阶段的模块示例。它主要作为启用多阶段初始化所需更改的说明而包含

#include <Python.h>

static int spam_exec(PyObject *module) {
    PyModule_AddStringConstant(module, "food", "spam");
    return 0;
}

#ifdef Py_mod_exec
static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};
#endif

static PyModuleDef spam_def = {
    PyModuleDef_HEAD_INIT,                      /* m_base */
    "spam",                                     /* m_name */
    PyDoc_STR("Utilities for cooking spam"),    /* m_doc */
    0,                                          /* m_size */
    NULL,                                       /* m_methods */
#ifdef Py_mod_exec
    spam_slots,                                 /* m_slots */
#else
    NULL,
#endif
    NULL,                                       /* m_traverse */
    NULL,                                       /* m_clear */
    NULL,                                       /* m_free */
};

PyMODINIT_FUNC
PyInit_spam(void) {
#ifdef Py_mod_exec
    return PyModuleDef_Init(&spam_def);
#else
    PyObject *module;
    module = PyModule_Create(&spam_def);
    if (module == NULL) return NULL;
    if (spam_exec(module) != 0) {
        Py_DECREF(module);
        return NULL;
    }
    return module;
#endif
}

内置模块

任何扩展模块都可以通过将其链接到可执行文件并将其包含在 inittab 中(无论是运行时使用 PyImport_AppendInittab,还是在配置时使用 freeze 等工具)作为内置模块使用。

为了保持这种可能性,本 PEP 中引入的所有对扩展模块加载的更改也将适用于内置模块。唯一的例外是非 ASCII 模块名称,如下所述。

子解释器和解释器重载

使用新初始化方案的扩展有望正确支持子解释器和多个 Py_Initialize/Py_Finalize 周期,避免 Python 文档 [6] 中提及的问题。该机制旨在使此操作变得简单,但扩展作者仍需小心。任何用户定义的函数、方法或实例都不能泄漏到不同的解释器。为了实现这一点,所有模块级状态都应保存在模块字典中,或保存在通过 PyModule_GetState 可访问的模块对象存储中。一个简单的经验法则是:不要定义任何静态数据,除了没有可变或用户可设置类属性的内置类型。

与多阶段初始化不兼容的函数

PyModule_Create 函数在具有非 NULL m_slots 指针的 PyModuleDef 结构上使用时,将失败。该函数无法访问多阶段初始化所需的 ModuleSpec 对象。

PyState_FindModule 函数将返回 NULL,PyState_AddModulePyState_RemoveModule 也将在具有非 NULL m_slots 的模块上失败。PyState 注册被禁用,因为可以从同一个 PyModuleDef 创建多个模块对象。

模块状态和 C 级回调

由于 PyState_FindModule 不可用,任何需要访问模块级状态(包括在模块级别定义的函数、类或异常)的函数都必须直接或间接接收模块对象(或它需要的特定对象)的引用。这在两种情况下目前很困难

  • 类的方法,它们接收对类的引用,但没有对类模块的引用
  • 具有 C 级回调的库,除非回调可以在回调注册时接收自定义数据

修复这些情况超出了本 PEP 的范围,但对于新机制对所有模块都有用是必要的。适当的修复已在 import-sig 邮件列表中讨论过 [5]

根据经验,目前依赖 PyState_FindModule 的模块不适合移植到新机制。

新函数

将添加一个新的函数和宏来实现模块创建阶段。这些类似于 PyModule_CreatePyModule_Create2,只是它们接受一个额外的 ModuleSpec 参数,并处理具有非 NULL 槽的模块定义

PyObject * PyModule_FromDefAndSpec(PyModuleDef *def, PyObject *spec)
PyObject * PyModule_FromDefAndSpec2(PyModuleDef *def, PyObject *spec,
                                    int module_api_version)

将添加一个新函数来实现模块执行阶段。这会分配每个模块状态(如果尚未分配),并 总是 处理执行槽。当模块执行时(除非模块正在重新加载),导入机制会调用此方法

PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def)

还将引入另一个函数来初始化 PyModuleDef 对象。这个幂等函数填充类型、引用计数和模块索引。它返回其参数转换为 PyObject* 的值,因此它可以直接从 PyInit 函数返回

PyObject * PyModuleDef_Init(PyModuleDef *);

此外,还将添加两个辅助函数,用于在模块上设置文档字符串和方法

int PyModule_SetDocString(PyObject *, const char *)
int PyModule_AddFunctions(PyObject *, PyMethodDef *)

导出钩子名称

由于可移植的 C 标识符仅限于 ASCII,因此模块名称必须进行编码以形成 PyInit 钩子名称。

对于 ASCII 模块名称,导入钩子名为 PyInit_<modulename>,其中 <modulename> 是模块的名称。

对于包含非 ASCII 字符的模块名称,导入钩子名为 PyInitU_<encodedname>,其中名称使用 CPython 的“punycode”编码(带有小写后缀的 Punycode)进行编码,并将连字符(“-”)替换为下划线(“_”)。

在 Python 中

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

示例

模块名称 初始化钩子名称
spam PyInit_spam
lančmít PyInitU_lanmt_2sa6t
スパム PyInitU_zck5b2b

对于非 ASCII 名称的模块,不支持单阶段初始化。

在本 PEP 的初始实现中,不支持具有非 ASCII 名称的内置模块。

模块重载

使用 importlib.reload() 重新加载扩展模块将继续无效,除了重新设置导入相关属性。

由于共享库加载的限制(POSIX 上的 dlopen 和 Windows 上的 LoadModuleEx),通常不可能在库更改后加载修改后的库。

除了尝试新版本的模块之外,重新加载的其他用例太少,无需所有模块作者都记住重新加载。如果需要类似重新加载的功能,作者可以为此导出一个专用函数。

一个库中的多个模块

为了在一个共享库中支持多个 Python 模块,该库除了导出与库文件名对应的 PyInit* 符号外,还可以导出额外的 PyInit* 符号。

请注意,此机制目前只能用于 加载 额外的模块,而不能用于 查找 它们。(这是加载器机制的限制,本 PEP 不尝试修改它。)为了解决缺少合适查找器的问题,可以使用以下代码

import importlib.machinery
import importlib.util
loader = importlib.machinery.ExtensionFileLoader(name, path)
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module

在支持符号链接的平台上,这些可以用于以多个名称安装一个库,将所有导出的模块暴露给正常的导入机制。

测试和初始实现

为了测试,将创建一个新的内置模块 _testmultiphase。该库将使用“一个库中的多个模块”中描述的机制导出几个额外的模块。

_testcapi 模块将保持不变,并将无限期地使用单阶段初始化(或直到不再支持)。

arrayxx* 模块将转换为使用多阶段初始化作为初始实现的一部分。

API 更改和新增功能摘要

新函数

  • PyModule_FromDefAndSpec(宏)
  • PyModule_FromDefAndSpec2
  • PyModule_ExecDef
  • PyModule_SetDocString
  • PyModule_AddFunctions
  • PyModuleDef_Init

新宏

  • Py_mod_create
  • Py_mod_exec

新类型

  • PyModuleDef_Type 将被公开

新结构

  • PyModuleDef_Slot

其他更改

PyModuleDef.m_reload 更改为 PyModuleDef.m_slots

BuiltinImporterExtensionFileLoader 现在将实现 create_moduleexec_module

内部 _imp 模块将具有向后不兼容的更改:将添加 create_builtincreate_dynamicexec_dynamic;将删除 init_builtinload_dynamic

未文档化的函数 imp.load_dynamicimp.init_builtin 将被向后兼容的垫片替换。

向后兼容性

现有模块将继续与新版本的 Python 源代码和二进制兼容。使用多阶段初始化的模块将不与未实现本 PEP 的 Python 版本兼容。

函数 init_builtinload_dynamic 将从 _imp 模块中删除(但不会从 imp 模块中删除)。

所有更改的加载器(BuiltinImporterExtensionFileLoader)将保持向后兼容;load_module 方法将由一个垫片替换。

Python/import.c 和 Python/importdl.c 的内部函数将被删除。(具体来说,它们是 _PyImport_GetDynLoadFunc_PyImport_GetDynLoadWindows_PyImport_LoadDynamicModule。)

可能的未来扩展

PEP 384PyType_Slot 启发的槽机制允许后续扩展。

一些扩展模块导出了许多常量;例如 _ssl 有一长串形式的调用

PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
                        PY_SSL_ERROR_ZERO_RETURN);

将其转换为声明性列表,类似于 PyMethodDef,将减少样板,并提供通常缺失的免费错误检查。

字符串常量和类型可以类似地处理。(请注意,类型的非默认基类无法静态可移植地指定;这种情况需要一个在槽添加之前运行的 Py_mod_exec 函数。尽管如此,免费错误检查仍然是有益的。)

另一种可能性是提供一个“main”函数,当模块通过 Python 的 -m 开关给出时运行。为此,runpy 模块需要进行修改,以利用 PEP 451 中引入的基于 ModuleSpec 的加载。此外,还需要添加一种机制来根据未最初定义的槽设置模块。

实施

正在进行中的实现可在 GitHub 存储库 [3] 中找到;补丁集在 [4]

以前的方法

Stefan Behnel 的初始原型 PEP [1] 有一个“PyInit_modulename”钩子,它会创建一个模块类,然后调用其 __init__ 来创建模块。这个提案与(当时不存在的)PEP 451 不符,后者将模块创建和初始化分解为不同的步骤。它也不支持将扩展加载到预先存在的模块对象中。

Alyssa (Nick) Coghlan 提出了“Create”和“Exec”钩子,并编写了一个原型实现 [2]。当时 PEP 451 尚未实现,因此原型未使用 ModuleSpec。

本 PEP 的原始版本使用了 CreateExec 钩子,并允许使用 Exec 钩子加载到任意预构建对象中。该提案使扩展模块初始化更接近 Python 模块的初始化方式,但后来认识到这不是一个重要的目标。当前的 PEP 描述了一个更简单的解决方案。

后续的迭代使用“PyModuleExport”钩子作为 PyInit 的替代,其中 PyInit 用于现有方案,PyModuleExport 用于多阶段。然而,无法根据模块名称确定钩子名称使得诸如 freeze 之类的工具自动生成 PyImport_Inittab 变得复杂。只保留 PyInit 钩子名称,即使它不完全适合导出定义,也产生了一个更简单的解决方案。

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-0489.rst

最后修改:2025-10-07 15:05:23 GMT