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 提议重新设计内置模块和扩展模块与导入机制交互的方式。这在 Python 3.0 中通过 PEP 3121 进行了最后一次修订,但当时并未解决所有问题。目标是通过使扩展模块更接近 Python 模块的行为方式来解决导入相关问题;特别是要挂接到 PEP 451 中引入的基于 ModuleSpec 的加载机制。
本提案借鉴了 PEP 384 的 PyType_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。
在使用多阶段初始化时,PyModuleDef
的 m_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_traverse、m_clear 和 m_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_AddModule
和 PyState_RemoveModule
也将在具有非 NULL m_slots 的模块上失败。PyState 注册被禁用,因为可以从同一个 PyModuleDef
创建多个模块对象。
模块状态和 C 级回调
由于 PyState_FindModule
不可用,任何需要访问模块级状态(包括在模块级别定义的函数、类或异常)的函数都必须直接或间接接收模块对象(或它需要的特定对象)的引用。这在两种情况下目前很困难
- 类的方法,它们接收对类的引用,但没有对类模块的引用
- 具有 C 级回调的库,除非回调可以在回调注册时接收自定义数据
修复这些情况超出了本 PEP 的范围,但对于新机制对所有模块都有用是必要的。适当的修复已在 import-sig 邮件列表中讨论过 [5]。
根据经验,目前依赖 PyState_FindModule
的模块不适合移植到新机制。
新函数
将添加一个新的函数和宏来实现模块创建阶段。这些类似于 PyModule_Create
和 PyModule_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
模块将保持不变,并将无限期地使用单阶段初始化(或直到不再支持)。
array
和 xx*
模块将转换为使用多阶段初始化作为初始实现的一部分。
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
。
BuiltinImporter
和 ExtensionFileLoader
现在将实现 create_module
和 exec_module
。
内部 _imp
模块将具有向后不兼容的更改:将添加 create_builtin
、create_dynamic
和 exec_dynamic
;将删除 init_builtin
、load_dynamic
。
未文档化的函数 imp.load_dynamic
和 imp.init_builtin
将被向后兼容的垫片替换。
向后兼容性
现有模块将继续与新版本的 Python 源代码和二进制兼容。使用多阶段初始化的模块将不与未实现本 PEP 的 Python 版本兼容。
函数 init_builtin
和 load_dynamic
将从 _imp
模块中删除(但不会从 imp
模块中删除)。
所有更改的加载器(BuiltinImporter
和 ExtensionFileLoader
)将保持向后兼容;load_module
方法将由一个垫片替换。
Python/import.c 和 Python/importdl.c 的内部函数将被删除。(具体来说,它们是 _PyImport_GetDynLoadFunc
、_PyImport_GetDynLoadWindows
和 _PyImport_LoadDynamicModule
。)
可能的未来扩展
受 PEP 384 的 PyType_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 的加载。此外,还需要添加一种机制来根据未最初定义的槽设置模块。
实施
以前的方法
Stefan Behnel 的初始原型 PEP [1] 有一个“PyInit_modulename
”钩子,它会创建一个模块类,然后调用其 __init__
来创建模块。这个提案与(当时不存在的)PEP 451 不符,后者将模块创建和初始化分解为不同的步骤。它也不支持将扩展加载到预先存在的模块对象中。
Alyssa (Nick) Coghlan 提出了“Create
”和“Exec
”钩子,并编写了一个原型实现 [2]。当时 PEP 451 尚未实现,因此原型未使用 ModuleSpec。
本 PEP 的原始版本使用了 Create
和 Exec
钩子,并允许使用 Exec
钩子加载到任意预构建对象中。该提案使扩展模块初始化更接近 Python 模块的初始化方式,但后来认识到这不是一个重要的目标。当前的 PEP 描述了一个更简单的解决方案。
后续的迭代使用“PyModuleExport
”钩子作为 PyInit
的替代,其中 PyInit
用于现有方案,PyModuleExport
用于多阶段。然而,无法根据模块名称确定钩子名称使得诸如 freeze 之类的工具自动生成 PyImport_Inittab
变得复杂。只保留 PyInit
钩子名称,即使它不完全适合导出定义,也产生了一个更简单的解决方案。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0489.rst