为什么 from ... import ... 语句包含隐式导入?

Why does the from ... import ... statement contain an implicit import?

给定一个包裹:

package/
├── __init__.py
└── module.py

__init__.py:

from .module import function

module.py:

def function():
    pass

可以导入包并打印其命名空间。

python -c 'import package; print(dir(package))'
['__builtins__', ..., 'function', 'module']

问题:

为什么 package 的命名空间包含 module__init__.py 中只导入了 function

我原以为 package 的命名空间只包含 function 而不是 moduleDocumentation,

中也提到了这个机制

"When a submodule is loaded using any mechanism (e.g. importlib APIs, the import or import-from statements, or built-in __import__()) a binding is placed in the parent module’s namespace to the submodule object."

但并不是真正有动力。对我来说,这个选择似乎很奇怪,因为我认为子模块是构建包的实现细节,并且不希望它们成为 API 的一部分,因为结构可以改变。

我也知道 "Python is for consenting adults" 并且无法真正向用户隐藏任何内容。但我认为,将子模块名称绑定到包的范围会使用户不太清楚 API 的实际部分以及可以更改的内容。

为什么不使用 __sub_modules__ 属性来让用户可以访问子模块?这个设计决定的原因是什么?

你说你认为子模块是实现细节。这不是子模块背后的设计意图;它们可以是,而且最常见的是,包的 public 接口的一部分。导入系统旨在促进子模块的访问,而不是阻止访问。

加载子模块会将绑定置于父级的名称空间中,因为这是访问模块所必需的。比如在下面的代码之后:

import package.submodule

表达式 package.submodule 的计算结果必须为子模块的模块对象。 package 评估为包的模块对象,因此此模块对象必须具有引用子模块的模块对象的 submodule 属性。

在这一点上,你几乎肯定会想,"hey, there's no reason from .submodule import function has to do the same thing!"它做同样的事情,因为这个属性绑定是子模块初始化的一部分,它只发生在第一次导入时,并且需要做同样的事情无论哪种导入都会触发设置。

这不是一个非常有力的理由。通过足够多的更改和调整,导入系统肯定可以 设计成您期望的方式。它不是那样设计的,因为设计师的优先级与你不同。 Python 的设计很少关心隐藏内容或支持任何隐私概念。

你必须明白 Python 是一种运行时语言。 defclassimport 都是可执行语句,它们在执行时将分别创建一个 functionclassmodule对象并将它们绑定到当前命名空间中。

wrt/ modules(包也是模块 - 至少在运行时),第一次为给定进程(直接或间接)导入模块时,匹配的 .py(好吧,通常它是编译的 .pyc version) 被执行(顶层的所有语句按顺序执行),生成的命名空间将用于填充 module 实例。只有完成此操作后,才能访问模块中定义的任何名称(您无法访问尚不存在的内容,可以吗?)。然后将模块对象缓存在 sys.modules 中以供后续导入。在此过程中,当加载子模块时,它被视为其父模块的属性。

For me this choice seems odd, as I think of sub-modules as implementation detail to structure packages and do not expect them to be part of the API as the structure can change

实际上,Python 的设计者考虑了相反的事情:"package"(注意在运行时没有 'package' 类型)主要是为了方便组织集合相关模块的 - IOW,̀moduleis the real building block - and as a matter of fact, at runtime, when what you import is technically a "package", it still materializes as amodule` 对象。

现在 "do not expect them to be part of the API as the structure can change",这当然已经被考虑在内了。这实际上是一种非常常见的模式,从单个模块开始,然后随着代码库的增长将其变成一个包——当然不会影响客户端代码。这里的关键是正确使用您的包的初始化程序 - __init__.py 文件 - 这实际上是您的包的 module 实例的构建来源。这让包充当 "facade",屏蔽 "implementation details" 的哪个子模块有效地定义了哪个函数,class 或其他。

所以这里的解决方案很简单,在你的包 __init__.py 中,1/ 导入你想要制作的名称 public (这样客户端代码可以直接从你的包导入而不是必须通过子模块)和 2/ 使用应被视为 public 的名称定义 __all__ 属性,以便清楚地记录接口。

FWIW,最后一个操作也应该对所有子模块完成,您也可以对真正 "implementation details" 的东西使用 _single_leading_underscore 命名约定。

None 这当然会阻止任何人直接从您的子模块导入甚至 "private" 名称,但是当出现问题时它们会自行解决("we are all consenting adults" 等)。