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 提议仅影响简单的非包模块。
考虑事项和先决条件
模块的 Pickling
Alyssa 提到了 issue 19702,该问题提议(引用自该问题)
- runpy 将确保当 __main__ 通过导入系统执行时,它也将在 sys.modules 中别名为 __spec__.name
- 如果设置了 __main__.__spec__,pickle 将使用 __spec__.name 而不是 __name__ 来 pickle 在 __main__ 中定义的类、函数和方法
- multiprocessing 已相应更新,以在父进程中设置 __main__.__spec__ 时跳过在子进程中创建 __mp_main__
上述第一点涵盖了本 PEP 的具体提案。
普通模块的 __name__ 不再是规范名称
Chris Angelico 指出,现在可以导入一个 __name__ 与您提供给“import”的名称不同的模块,因为“__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 参考实现的 issue tracker 条目,当前的 PR 草案可在 GitHub 上找到。
开放问题
这项提案确实引起了一些向后兼容性问题,这些问题需要充分理解,并设计弃用过程,或提供明确的移植指南。
Pickle 兼容性
如果不对 pickle 模块进行任何更改,那么以前使用正确模块名(由于双重导入)编写的 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 命令行选项存在以来一直存在,并且经常(尽管不频繁)被其他人遇到。
除了 issue 19702 之外,关于 __main__ 的差异在 PEP 451 中有所提及,并且一个类似的提案(早于 PEP 451)在 PEP 395 的 修复主模块双重导入 一节中进行了描述。
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0499.rst
最后修改: 2025-02-01 08:55:40 GMT