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 的环境中,名称解析在相对导入中的工作方式,因此不可能在包内执行模块。这源于这样一个事实:被执行为主“main”模块的模块会将其 __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 中的位置(如果存在的话)。仅 stat 调用就可能非常昂贵,如果被执行脚本所在的​​文件系统是 NFS 之类的。

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

实施

当使用 -m 选项时,sys.main 将被设置为传入的参数。 sys.argv 将被调整,就像它当前一样。然后将发生等同于 __import__(self.main) 的操作。这与当前语义不同,因为 runpy 模块会获取模块名指定的文件的代码对象,以显式设置 __name__ 和其他属性。这种情况不再需要,因为导入机制可以执行其正常操作。

如果指定了文件名,则 sys.main 将被设置为 "__main__"。然后将读取指定的文件,创建代码对象,然后使用 __name__ 设置为 "__main__" 的方式执行它。这与当前语义一致。

过渡计划

为了使 Python 2.6 能够同时支持当前语义和拟议语义,sys.main 将始终设置为 "__main__"。否则,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__ 属性。对于作为主模块执行的模块,该属性将具有 true 值,而所有其他模块将具有 false 值。这会带来一个很好的结果,即主模块代码习惯被简化为:

if __main__:
    ...

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

使用 __file__ 而不是 __name__

任何提议都可以通过使用模块上的 __file__ 属性而不是 __name__ 来实现,包括当前语义。但这存在的问题是,在拟议的解决方案中,存在模块没有定义 __file__ 属性,或者 __file__ 属性的值与其他模块相同。

当前语义出现的问题是,仍然必须尝试将文件路径解析为模块名称才能使导入工作。

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

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

这被拒绝了,因为它似乎太像一个 hack。

参考资料


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

最后修改:2025-02-01 08:59:27 GMT