PEP 338 – 将模块作为脚本执行
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2004年10月16日
- Python 版本:
- 2.5
- 发布历史:
- 2004年11月8日,2006年2月11日,2006年2月12日,2006年2月18日
摘要
本PEP定义了将任何Python模块作为脚本执行的语义,无论是使用-m命令行开关,还是通过调用runpy.run_module(modulename)。
Python 2.4中实现的-m开关功能相当有限。本PEP提议利用PEP 302导入钩子,允许执行任何提供对其代码对象访问的模块。
基本原理
Python 2.4添加了命令行开关-m,允许使用Python模块命名空间定位模块以作为脚本执行。其动机示例是标准库模块,例如pdb和profile,而Python 2.4的实现对于这个有限的目的来说是很好的。
许多用户和开发人员要求扩展此功能,以支持运行位于包内的模块。提供的一个示例是pychecker的pychecker.checker模块。此功能在Python 2.4的实现中被省略,因为其实现要复杂得多,并且最合适的策略完全不清楚。
python-dev上的意见是,最好将扩展推迟到Python 2.5,并经过PEP流程,以确保我们做对了。
从那时起,也有人指出,当前版本的-m不支持zipimport或任何其他类型的替代导入行为(例如冻结模块)。
将此功能作为Python模块提供比用C语言编写要容易得多,并且使所有Python程序都可以轻松使用此功能,而不仅仅是CPython解释器。CPython的命令行开关可以重写以利用新模块。
执行其他脚本的脚本(例如profile,pdb)也可以选择使用新模块来提供-m风格的支持,以识别要执行的脚本。
本提案的范围
在Python 2.4中,使用-m定位的模块的执行方式,就好像其文件名已在命令行上提供一样。本PEP的目标是尽可能地使该语句对于包内的模块或通过替代导入机制(例如zipimport)访问的模块也成立。
之前的讨论表明,应该指出本PEP**不是**关于改变使Python模块也可用作脚本的惯用语(参见PEP 299)。该问题被认为与本PEP解决的特定功能正交。
当前行为
在描述新语义之前,有必要介绍一下Python 2.4的现有语义(因为它们目前仅由源代码和命令行帮助定义)。
当在命令行上使用-m时,它会立即终止选项列表(像-c一样)。参数被解释为顶级Python模块的名称(即可以在sys.path上找到的模块)。
如果找到该模块,并且其类型为PY_SOURCE或PY_COMPILED,则命令行实际上从python <options> -m <module> <args>重新解释为python <options> <filename> <args>。这包括正确设置sys.argv[0](有些脚本依赖于此——Python自己的regrtest.py就是一个例子)。
如果未找到模块,或类型不正确,则会打印错误。
提议的语义
提议的语义相当简单:如果使用-m执行模块,则使用PEP 302导入机制来定位模块并检索其编译代码,然后根据顶级模块的语义执行该模块。解释器通过调用新的标准库函数runpy.run_module来实现此目的。
这是由于Python的导入机制在包内定位模块的方式所必需的。包可以在初始化期间修改其自身的__path__变量。此外,路径可能受*.pth文件的影响,并且某些包会在sys.metapath上安装自定义加载器。因此,Python可靠地定位模块的唯一方法是导入包含包并使用PEP 302导入钩子来访问Python代码。
请注意,定位要执行的模块的过程可能需要导入包含包。此类包导入对执行模块可见的影响是
- 包含包将在sys.modules中
- 包初始化的任何外部影响(例如已安装的导入钩子、日志记录器、atexit处理程序等)
参考实现
SourceForge上提供了参考实现([2]),以及库参考文档([5])。此实现分为两部分。第一部分是提议的标准库模块runpy。第二部分是对实现-m开关的代码的修改,使其始终委托给runpy.run_module,而不是直接运行模块。委托的形式为
runpy.run_module(sys.argv[0], run_name="__main__", alter_sys=True)
run_module是runpy在其公共API中公开的唯一函数。
run_module(mod_name[, init_globals][, run_name][, alter_sys])
执行指定模块的代码并返回结果模块的全局字典。模块的代码首先使用标准导入机制定位(详情请参阅PEP 302),然后在新的模块命名空间中执行。可选字典参数
init_globals可在执行代码之前用于预填充全局字典。提供的字典不会被修改。如果提供的字典中定义了以下任何特殊全局变量,则这些定义将被run_module函数覆盖。在执行模块代码之前,特殊全局变量
__name__、__file__、__loader__和__builtins__将在全局字典中设置。如果提供了可选参数
run_name,则__name__设置为run_name,否则设置为原始的mod_name参数。
__loader__设置为用于检索模块代码的PEP 302模块加载器(此加载器可能是标准导入机制的包装器)。
__file__设置为模块加载器提供的名称。如果加载器未提供文件名信息,则此参数设置为None。
__builtins__会自动使用对__builtin__模块的顶级命名空间的引用进行初始化。如果提供了参数
alter_sys并求值为True,则sys.argv[0]将更新为__file__的值,并且sys.modules[__name__]将更新为正在执行的模块的临时模块对象。在函数返回之前,sys.argv[0]和sys.modules[__name__]都将恢复为原始值。
当作为脚本调用时,runpy模块会找到并执行作为第一个参数提供的模块。它通过删除sys.argv[0](它引用runpy模块本身)来调整sys.argv,然后调用run_module(sys.argv[0], run_name="__main__", alter_sys=True)。
导入语句和主模块
2.5b1的发布显示了本PEP和PEP 328之间令人惊讶(但事后看来是显而易见的)的交互——显式相对导入在主模块中不起作用。这是因为相对导入依赖于__name__来确定当前模块在包层次结构中的位置。在主模块中,__name__的值总是'__main__',因此显式相对导入总是会失败(因为它们只适用于包内的模块)。
调查为什么当主模块直接执行时隐式相对导入**似乎**有效,但使用-m执行时失败,结果表明此类导入实际上总是被视为绝对导入。由于直接执行的工作方式,包含被执行模块的包被添加到sys.path中,因此其同级模块实际上被作为顶级模块导入。如果将隐式相对导入用于可能直接执行的模块(例如测试模块或实用脚本),则这很容易导致应用程序中出现同级模块的多个副本。
对于2.5版本,建议在任何旨在用作主模块的模块中始终使用绝对导入。-m开关在这里提供了一个优势,因为它将当前目录插入到sys.path中,而不是包含主模块的目录。这意味着只要当前目录包含包的顶级目录,就可以使用-m从包内部运行模块。即使包没有安装在sys.path上的其他任何位置,绝对导入也能正常工作。如果模块直接执行并使用绝对导入来检索其同级模块,那么顶级包目录需要安装在sys.path上的某个位置(因为当前目录不会自动添加)。
下面是一个文件布局示例
devel/
pkg/
__init__.py
moduleA.py
moduleB.py
test/
__init__.py
test_A.py
test_B.py
只要当前目录是devel,或者devel已经在sys.path上,并且测试模块使用绝对导入(例如import pkg moduleA来检索被测试模块),PEP 338允许测试运行如下
python -m pkg.test.test_A
python -m pkg.test.test_B
关于在使用-m执行主模块时是否应该支持相对导入的问题,将在Python 2.6中重新讨论。允许它将需要更改Python的导入语义或用于指示模块何时为主模块的语义,因此这不是一个草率的决定。
已解决的问题
runpy模块的开发受到了一些关键设计决策的影响。这些列在下面。
- 特殊变量
__name__、__file__和__loader__在模块执行之前设置在模块的全局命名空间中。由于run_module会更改这些值,因此它**不会**修改提供的字典。如果它修改了,那么将globals()传递给此函数可能会产生不良副作用。 - 有时,填充特殊变量所需的信息根本不可用。与其试图过于聪明,不如在无法确定相关信息时,将这些变量简单地设置为
None。 - 对alter_sys参数没有特殊保护。如果文件名信息不可用,这可能导致
sys.argv[0]设置为None。 - 导入锁不用于避免在alter_sys设置为True时出现潜在的线程问题。相反,建议线程代码只需避免使用此标志。
备选方案
考虑的第一个替代实现忽略了包的__path__变量,并且只在主包目录中查找。这种行为的Python脚本可以在execmodule食谱的讨论中找到[3]。
execmodule食谱本身是本PEP早期版本中提议的机制(在PEP作者阅读PEP 302之前)。
这两种方法都被拒绝,因为它们不符合-m开关的主要目标——允许使用完整的Python命名空间从命令行定位要执行的模块。
本PEP的早期版本包含一些关于exec如何处理局部变量字典和来自函数对象的代码的错误假设。这些错误假设导致了一些不必要的设计复杂性,这些复杂性现在已被删除——run_code与exec共享所有怪癖。
PEP的早期版本还公开了一个比实现-m开关更新所需的单个run_module()函数更广泛的API。为了简洁起见,这些额外的函数已从提议的API中删除。
在 SVN 中的最初实现之后,很明显在执行初始应用程序脚本时持有导入锁是不正确的(例如,python -m test.regrtest test_threadedimport 失败)。因此,run_module 函数只在实际搜索模块时持有导入锁,并在执行前释放它,即使设置了 alter_sys。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0338.rst
最后修改: 2025-02-01 08:59:27 GMT