PEP 499 – python -m foo
也应该在 sys.modules
中绑定 'foo'
- 作者:
- Cameron Simpson <cs at cskk.id.au>,Chris Angelico <rosuav at gmail.com>,Joseph Jevnik <joejev at gmail.com>
- BDFL 代表:
- Alyssa Coghlan
- 状态:
- 已延期
- 类型:
- 标准跟踪
- 创建日期:
- 2015 年 8 月 7 日
- Python 版本:
- 3.10
PEP 延期
目前预计该 PEP 的实现不会在 2020 年 4 月的 Python 3.9 功能冻结之前准备好,因此已将其推迟 12 个月到 Python 3.10。
摘要
当模块在 Python 命令行上用作主程序时,例如通过
python -m module.name …
如果该模块在程序中再次导入,则很容易意外地得到该模块的两个独立实例。本 PEP 提出了一种解决此问题的方法。
当模块通过 Python 的 -m 选项调用时,该模块会绑定到 sys.modules['__main__']
,并且其 .__name__
属性设置为 '__main__'
。这使得许多模块底部的标准“主程序”样板代码成为可能,例如
if __name__ == '__main__':
sys.exit(main(sys.argv))
但是,当使用上述命令行调用时,自然会推断该模块实际上是在其官方名称 module.name
下导入的,因此如果程序再次导入该名称,则会获得相同的模块实例。
实际上,该模块仅作为 '__main__'
导入。另一个导入将获得一个不同的模块实例,这可能导致令人困惑的错误,所有这些错误都源于模块全局对象的两个实例:每个模块中一个。
示例包括
- 模块级数据结构
- 某些模块提供诸如缓存或注册表之类的功能作为模块级全局变量,通常是私有的。模块的第二个实例创建第二个数据结构。如果该结构是缓存(例如在
re
模块中),则存在两个缓存,导致内存浪费。如果该结构是共享注册表(例如值到处理程序的映射),则可以将处理程序注册到一个注册表,并尝试通过另一个注册表使用它,在该注册表中,它未知。 - 哨兵
- 由模块提供的哨兵值的标准测试是使用
is
进行身份比较,因为这避免了不可靠的“看起来像”比较(例如相等性),这些比较既可以将两个值错误地匹配为“相等”(例如为零值)或在对象不兼容时引发TypeError
。当模块有两个实例时,存在两个哨兵实例,并且只有一个可以通过is
识别。 - 类
- 使用两个模块时,会存在任何提供的类的重复类定义。所有依赖于识别这些类及其子类的操作都可能发生故障,具体取决于从哪个模块获取参考类(来自其中一个模块)以及从何处获取比较类或实例。这会影响
isinstance
、issubclass
以及try
/except
结构。
提案
建议要解决这种情况,只需简单地更改 -m
选项的实现方式即可:除了将模块对象绑定到 sys.modules['__main__']
之外,还将其绑定到 sys.modules['module.name']
。
Alyssa (Nick) Coghlan 建议这就像修改 runpy
模块的 _run_module_as_main
函数一样简单
main_globals = sys.modules["__main__"].__dict__
改为
main_module = sys.modules["__main__"]
sys.modules[mod_spec.name] = main_module
main_globals = main_module.__dict__
Joseph Jevnik 指出,作为包的模块已经执行了与此提案非常类似的操作:__init__.py 文件绑定到模块的规范名称,__main__.py 文件绑定到“__main__”。因此,不会出现双重导入问题。因此,本 PEP 提出仅影响简单的非包模块。
考虑因素和先决条件
模块的序列化
Alyssa 提到了 问题 19702,该问题建议(引自该问题)
- runpy 将确保当 __main__ 通过导入系统执行时,它也会在 sys.modules 中作为 __spec__.name 存在别名
- 如果设置了 __main__.__spec__,则 pickle 将使用 __spec__.name 而不是 __name__ 来序列化在 __main__ 中定义的类、函数和方法
- 多处理已适当地更新,以便在父进程中设置了 __main__.__spec__ 时,在子进程中跳过创建 __mp_main__
上面第一点涵盖了本 PEP 的具体提案。
普通模块的 __name__
不再是规范的
Chris Angelico 指出,可以导入一个 __name__
不是您提供给“导入”的模块,因为“__main__”现在存在于“module.name”中,因此随后的 import module.name
会发现它已经存在。因此,对于某些普通导入,__name__
不再是规范名称。
以下是一些反驳意见
- 从 PEP 451 开始,模块的规范名称存储在
__spec__.name
中。 - 实际上很少有代码会关心
__name__
是否是规范名称,任何确实关心的代码都应该更新为查询__spec__.name
,并为较旧的 Python 回退到__name__
(如果相关)。即使不批准本 PEP,情况也是如此。 - 如果批准本 PEP,则可以通过其规范名称内省模块并通过从
__name__
推断来询问“这是主程序吗?”。以前这是不可能的。
明显的反例是标准的“我是主程序吗?”样板,其中 __name__
预期为“__main__”。本 PEP 明确保留了该语义。
参考实现
BPO 36375 是 PEP 参考实现的问题跟踪器条目,当前的草稿 PR 可在 GitHub 上找到。
未解决的问题
此提案确实引发了一些向后兼容性问题,这些问题需要充分理解,并设计一个弃用过程,或提供清晰的移植指南。
序列化兼容性
如果不对 pickle 模块进行任何更改,则以前使用正确的模块名称(由于双重导入)编写的序列化数据可能会开始使用 __main__
作为其模块名称,因此其他项目无法正确加载。
要检查的场景
python script.py
编写,python -m script
读取python -m script
编写,python script.py
读取python -m script
编写,python some_other_app.py
读取old_python -m script
编写,new_python -m script
读取new_python -m script
编写,old_python -m script
读取
特殊处理 __main__
的项目
为了使回归测试套件通过,当前的参考实现不得不修补 pdb
以避免破坏其自己的全局命名空间。
这表明可能存在更广泛的兼容性问题,其中某些脚本依赖于直接执行和导入提供不同的命名空间(就像包执行通过在 __main__ 命名空间中执行 __main__ 子模块来保持两者分离一样,而包名称照常引用 __init__ 文件)。
背景
我在调试主程序时偶然发现了此问题,该程序通过一个尝试修补命名模块(即主程序模块)的模块进行调试。当然,修补无效,因为它按名称导入了主模块,因此修补了第二个模块实例,而不是正在运行的模块实例。
但是,该问题一直存在于 -m
命令行选项存在以来,并且其他人经常(尽管不频繁)遇到该问题。
除了 问题 19702 之外,围绕 __main__
的差异在 PEP 451 中有所提及,并且一个类似的提案(早于 PEP 451)在 PEP 395 中的 修复主模块的双重导入 部分进行了描述。
版权
本文档已置于公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0499.rst
上次修改时间:2023-10-11 12:05:51 GMT