Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 识别。
使用两个模块时,会存在任何提供的类的重复类定义。所有依赖于识别这些类及其子类的操作都可能发生故障,具体取决于从哪个模块获取参考类(来自其中一个模块)以及从何处获取比较类或实例。这会影响 isinstanceissubclass 以及 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