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__
在执行模块代码之前在全局字典中设置。
__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