PEP 547 – 使用 -m 选项运行扩展模块
- 作者:
- Marcel Plch <gmarcel.plch at gmail.com>,Petr Viktorin <encukou at gmail.com>
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 创建日期:
- 2017年5月25日
- Python 版本:
- 3.7
- 发布历史:
推迟通知
Cython——此 PEP 最重要的用例,也是唯一明确的用例——尚未为多阶段初始化做好准备。它在 C 级静态变量中保留全局状态。请参阅 Cython issue 1923 中的讨论。
在情况改变之前,此 PEP 将被推迟。
摘要
此 PEP 提出了允许内置模块和扩展模块使用 PEP 489 多阶段初始化在 __main__ 命名空间中执行的实现。
通过此功能,启用多阶段初始化的模块可以使用以下命令运行
$ python3 -m _testmultiphase
This is a test module named __main__.
动机
目前,扩展模块不支持 Python 源模块的所有功能。具体来说,无法使用 Python 的 -m 选项将扩展模块作为脚本运行。
实现此目的的技术基础已为 PEP 489 完成,并且在那个 PEP 的“未来可能扩展”部分列出了启用 -m 选项。从技术上讲,此处提出的额外更改相对较小。
基本原理
扩展模块缺乏对 -m 选项的支持,传统上通过提供一个 Python 包装器来解决。例如,_pickle 模块的命令行界面在纯 Python pickle 模块中(以及纯 Python 的重新实现)。
这对于标准库模块来说效果很好,因为使用 C API 构建命令行界面很麻烦。然而,其他用户可能希望直接创建可执行的扩展模块。
一个重要的用例是 Cython,一种类似 Python 的语言,它编译成 C 扩展模块。Cython 是 Python 的一个(近乎)超集,这意味着用 Cython 编译 Python 模块通常不会改变模块的功能,允许逐步添加 Cython 特有的功能。这个 PEP 将允许 Cython 扩展模块在使用 -m 选项运行时,其行为与对应的 Python 模块相同。Cython 开发者认为此功能值得实现(参见 Cython issue 1715)。
背景
Python 的 -m 选项由函数 runpy._run_module_as_main 处理。
由 -m 指定的模块不会正常导入。相反,它在 __main__ 模块的命名空间中执行,该模块在解释器初始化早期创建。
对于 Python 源模块,在另一个模块的命名空间中运行不是问题:代码执行时 locals 和 globals 都设置为现有模块的 __dict__。对于扩展模块则不然,其 PyInit_* 入口点传统上既创建了一个新的模块对象(使用 PyModule_Create),又对其进行了初始化。
自 Python 3.5 起,扩展模块可以使用 PEP 489 多阶段初始化。在此场景下,PyInit_* 入口点返回一个 PyModuleDef 结构体:描述模块应如何创建和初始化的信息。扩展可以选择使用 Py_mod_create 回调来定制模块对象的创建,或者通过不指定 Py_mod_create 来选择使用普通模块对象。另一个回调 Py_mod_exec 随后被调用以初始化模块对象,例如通过用方法和类填充它。
提案
多阶段初始化使得在另一个模块的命名空间中执行扩展模块成为可能:如果未指定 Py_mod_create 回调,则可以将 __main__ 模块传递给 Py_mod_exec 回调进行初始化,就好像 __main__ 是一个全新构建的模块对象一样。
此方案中的一个复杂之处是 C 级模块状态。每个模块都有一个 md_state 指针,指向创建扩展模块时分配的内存区域。PyModuleDef 指定了要分配多少内存。
实现必须确保 md_state 内存最多只分配一次。此外,Py_mod_exec 回调应该每个模块只调用一次。多次初始化模块的影响过于微妙,不应期望扩展作者对其进行推断。md_state 指针本身将作为防护:分配内存和调用 Py_mod_exec 将始终同时进行,如果 md_state 已经非 NULL,则初始化扩展模块将失败。
由于 __main__ 模块不是作为扩展模块创建的,因此其 md_state 通常为 NULL。在 __main__ 的上下文中初始化扩展模块之前,其模块状态将根据该模块的 PyModuleDef 进行分配。
虽然 PEP 489 旨在使这些更改普遍可行,但有必要将扩展模块的模块发现、创建和初始化步骤解耦,以便可以使用另一个模块而不是新初始化的模块,并且需要将此功能添加到 runpy 和 importlib 中。
规范
将为 importlib 加载器添加一个可选的新方法。此方法将命名为 exec_in_module,并接受两个位置参数:模块规范和一个已存在的模块。模块上已设置的任何导入相关属性,例如 __spec__ 或 __name__,将被忽略。
runpy._run_module_as_main 函数将寻找这个新的加载器方法。如果它存在,runpy 将执行它,而不是尝试加载和运行模块的 Python 代码。否则,runpy 将像以前一样操作。
ExtensionFileLoader 变更
importlib 的 ExtensionFileLoader 将获得 exec_in_module 的实现,该实现将调用一个新函数 _imp.exec_in_module。
_imp.exec_in_module 将使用现有机制查找并调用扩展模块的 PyInit_* 函数。
PyInit_* 函数可以返回一个完全初始化的模块(单阶段初始化)或一个 PyModuleDef(用于 PEP 489 多阶段初始化)。
在单阶段初始化情况下,_imp.exec_in_module 将引发 ImportError。
在多阶段初始化情况下,PyModuleDef 和待初始化的模块将被传递给一个新的函数 PyModule_ExecInModule。
如果 PyModuleDef 指定了 Py_mod_create 槽位,或者模块已经初始化(即其 md_state 指针不是 NULL),此函数将引发 ImportError。否则,该函数将根据 PyModuleDef 初始化模块。
向后兼容性
此 PEP 保持向后兼容性。它只添加了新函数,以及为之前不支持将模块作为 __main__ 运行的加载器添加了一个新的加载器方法。
参考实现
此 PEP 的参考实现可在 GitHub 上找到。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0547.rst