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问题1923中的讨论。
在情况发生变化之前,PEP将被延期。
摘要
本PEP建议实现允许使用PEP 489多阶段初始化在__main__
命名空间中执行内置模块和扩展模块。
这样,可以使用以下命令运行启用多阶段初始化的模块
$ python3 -m _testmultiphase
This is a test module named __main__.
动机
目前,扩展模块不支持Python源模块的所有功能。具体来说,无法使用Python的-m
选项将扩展模块作为脚本运行。
为实现此目标,PEP 489已经完成了技术基础工作,并且在该PEP的“可能的未来扩展”部分中列出了启用-m
选项。从技术上讲,此处提出的额外更改相对较小。
基本原理
传统上,通过提供Python包装器来解决扩展模块不支持-m
选项的问题。例如,_pickle
模块的命令行界面位于纯Python pickle
模块中(以及纯Python重新实现)。
这对于标准库模块非常有效,因为使用C API构建命令行界面很麻烦。但是,其他用户可能希望直接创建可执行的扩展模块。
一个重要的用例是Cython,这是一种类似Python的语言,它编译成C扩展模块。Cython是Python的(近乎)超集,这意味着使用Cython编译Python模块通常不会更改模块的功能,从而允许逐步添加Cython特定的功能。本PEP将允许Cython扩展模块在使用-m
选项运行时表现与其Python对应模块相同。Cython开发人员认为该功能值得实现(请参阅Cython问题1715)。
背景
Python的-m
选项由函数runpy._run_module_as_main
处理。
由-m
指定的模块不会正常导入。相反,它在__main__
模块的命名空间中执行,该模块是在解释器初始化的早期创建的。
对于Python源模块,在另一个模块的命名空间中运行不是问题:代码使用设置为现有模块__dict__
的locals
和globals
执行。对于扩展模块并非如此,扩展模块的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
上次修改时间:2023年9月9日17:39:29 GMT