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 个部分。在这个例子中,ham
和 beans
都被丢弃,而 spam
与剩余部分(bacon
)连接在一起。这导致正确导入模块 bacon.spam
。
在处理相对导入时,这种对模块的 __name__
属性的依赖性在包中执行脚本时成为问题。由于执行脚本的名称被设置为 '__main__'
,导入无法解析任何相对导入,从而导致 ImportError
。
例如,假设我们有一个名为 bacon
的包,其中包含一个 __init__.py
文件
from . import spam
还在 bacon
包中创建一个名为 spam
的模块(它可以是一个空文件)。现在,如果你尝试执行 bacon
包(通过 python bacon/__init__.py
或 python -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