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

Python 增强提案

PEP 3122 – 主模块的界定

作者:
Brett Cannon
状态:
已拒绝
类型:
标准跟踪
创建:
2007 年 4 月 27 日
发布历史:


目录

注意

此 PEP 已被拒绝。Guido 认为在包中运行脚本是一种反模式 [3].

摘要

由于名称解析在 PEP 328 实现的世界中的相对导入工作方式,因此在包中执行模块的能力不再可能。这种失败源于这样一个事实:作为“主”模块执行的模块将其 __name__ 属性替换为 "__main__",而不是将其保留为模块的绝对名称。这破坏了导入将相对导入从主模块解析为绝对名称的能力。

为了解决这个问题,此 PEP 提议改变主模块的界定方式。通过保留模块中的 __name__ 属性并设置 sys.main 为主模块的名称,这将允许至少在某些情况下在使用相对导入的包中执行模块。

此 PEP 并没有解决引入模块级函数的想法,该函数像 PEP 299 提议的那样自动执行。

问题

随着 PEP 328 的引入,相对导入变得依赖于执行导入的模块的 __name__ 属性。这是因为相对导入中点的使用用于剥离调用模块名称的各个部分,以计算导入应该在包层次结构中的哪个位置(在 PEP 328 之前,相对导入可能会失败,并且会回退到绝对导入,这些导入有成功的机会)。

例如,考虑从 bacon.ham.beans 模块(bacon.ham.beans 本身不是包,即没有定义 __path__)执行的导入 from .. import spam。相对导入的名称解析采用调用者的名称(bacon.ham.beans),以点分割,然后根据级别(为 2)切除最后 n 个部分。在这个例子中,hambeans 都被丢弃,而 spam 与剩余部分(bacon)连接在一起。这导致正确导入模块 bacon.spam

在处理相对导入时,这种对模块的 __name__ 属性的依赖性在包中执行脚本时成为问题。由于执行脚本的名称被设置为 '__main__',导入无法解析任何相对导入,从而导致 ImportError

例如,假设我们有一个名为 bacon 的包,其中包含一个 __init__.py 文件

from . import spam

还在 bacon 包中创建一个名为 spam 的模块(它可以是一个空文件)。现在,如果你尝试执行 bacon 包(通过 python bacon/__init__.pypython -m bacon),你将收到一个关于尝试从非包中进行相对导入的 ImportError。显然,导入是有效的,但由于将 __name__ 设置为 '__main__',导入认为 bacon/__init__.py 不在包中,因为 __name__ 中不存在任何点。要详细了解算法的工作原理,请参见沙箱中的 importlib.Import._resolve_name() [2].

目前,一个解决方法是删除正在执行的模块中的所有相对导入,并将它们改为绝对导入。不过,这很不幸,因为不应该要求使用特定类型的资源才能使包中的模块能够执行。

解决方案

问题的解决方案是不改变模块中 __name__ 的值。但仍然需要一种方法来让执行代码知道它正在作为脚本执行。这通过 sys 模块中名为 main 的新属性来处理。

当模块作为脚本执行时,sys.main 将设置为模块的名称。这改变了当前的习惯用法

if __name__ == '__main__':
    ...

import sys
if __name__ == sys.main:
    ...

新提出的解决方案确实引入了额外的样板代码,即模块导入。但由于该解决方案没有引入新的内置函数或模块属性(如 被拒绝的想法 中所讨论的),因此被认为值得增加这一行。

提出的解决方案的另一个问题(也适用于所有被拒绝的想法)是它没有直接解决发现文件名称的问题。考虑 python bacon/spam.py。仅从文件名来看,无法确定 bacon 是否是一个包。为了正确地找出这一点,当前目录必须存在于 sys.path 上,并且必须存在 bacon/__init__.py

但这只是一个简单的例子。考虑 python ../spam.py。仅从文件名来看,无法确定 spam.py 是否在一个包中。一个可能的解决方案是找出 .. 的绝对名称,检查是否存在名为 __init__.py 的文件,然后查看该目录是否在 sys.path 上。如果不是,则继续向上遍历目录,直到不再找到 __init__.py 文件或在 sys.path 上找到目录。

这可能是一个昂贵的过程。如果包深度很深,则可能需要大量的磁盘访问才能发现包在 sys.path 上的锚定位置(如果有的话)。如果执行脚本所在的系统是像 NFS 这样的文件系统,那么仅状态调用就很昂贵。

由于这些问题,只有在使用 -m 命令行参数(由 PEP 338 引入)时,才会设置 __name__。否则,将设置 __name__"__main__" 的回退语义将发生。无论 __name__ 设置为什么,sys.main 仍将设置为正确的值。

实现

使用 -m 选项时,sys.main 将设置为传入的参数。sys.argv 将根据当前情况进行调整。然后执行相当于 __import__(self.main) 的操作。这与当前语义不同,因为 runpy 模块获取由模块名称指定的文件的代码对象,以便显式地设置 __name__ 和其他属性。这不再需要,因为导入可以在这种情况下执行其正常操作。

如果指定了文件名,则 sys.main 将设置为 "__main__"。然后读取指定的文件,创建代码对象,并使用设置为 "__main__"__name__ 执行该代码对象。这反映了当前的语义。

过渡计划

为了使 Python 2.6 能够支持当前语义和提议的语义,sys.main 将始终设置为 "__main__"。否则,Python 2.6 将不会发生任何变化。这不幸地意味着 Python 2.6 不会从这种变化中获益,但它最大程度地提高了代码与 2.6 和 3.0 的兼容性。

为了帮助过渡到新的习惯用法,2to3 [1] 将获得一个规则,将当前的 if __name__ == '__main__': ... 习惯用法转换为新的习惯用法。但是,这不会帮助在习惯用法之外检查 __name__ 的代码。

被拒绝的想法

__main__ 内置函数

一个反提案是引入一个名为 __main__ 的内置函数。内置函数的值将是正在执行的模块的名称(就像提议的 sys.main 一样)。这将导致新的习惯用法

if __name__ == __main__:
    ...

一个缺点是语法上的差异很微妙;即“__main__”周围引号的省略。有些人认为,对于现有的 Python 程序员来说,这会导致在意外地添加引号时引入错误。但可以争辩说,由于这是一个非常浅层的错误,因此可以通过测试很快发现错误。

虽然内置名称可以明显不同(例如,main),但另一个缺点是它引入了新的内置函数。由于可以通过简单的解决方案,例如 sys.main 来实现,而无需在 Python 中添加另一个内置函数,因此该提案被拒绝了。

__main__ 模块属性

另一个提案是在每个模块中添加一个 __main__ 属性。对于作为主模块执行的模块,该属性的值为真,而所有其他模块的值为假。这有一个很好的结果,即简化了主模块习惯用法为

if __main__:
    ...

缺点是引入了新的模块属性。它还需要比提议的解决方案更深入地与导入机制集成。

使用 __file__ 而不是 __name__

任何提案都可以改为使用模块上的 __file__ 属性而不是 __name__,包括当前语义。这方面的问题在于,对于提议的解决方案,模块没有定义 __file__ 属性,或者其值与其他模块相同。

当前语义出现的问题是,为了使导入正常工作,你仍然需要尝试将文件路径解析为模块名称。

用于 __name__ 的特殊字符串子类,覆盖 __eq__

一个提案是定义 str 的子类,该子类覆盖 __eq__ 方法,以便它与 "__main__" 以及模块的实际名称相比较。在所有其他方面,该子类与 str 相同。

这被拒绝了,因为它看起来太像一个 hack 了。

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-3122.rst

最后修改:2023-09-09 17:39:29 GMT