【问题标题】:Why does the from ... import ... statement contain an implicit import?为什么 from ... import ... 语句包含隐式导入?
【发布时间】:2020-04-26 00:02:19
【问题描述】:

给定一个包裹:

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']

问题:

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

我原以为package 的命名空间将只包含function 而不是module。这个机制在Documentation中也有提到,

"当使用任何机制加载子模块时(例如 importlib API, import 或 import-from 语句,或内置 __import__()) a 绑定被放置在父模块的命名空间到子模块 对象。”

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

我还知道“Python 是为成年人所用的”,并且无法真正向用户隐藏任何内容。但我会争辩说,将子模块名称绑定到包的范围会使用户不太清楚什么是 API 的实际部分以及可以更改的内容。

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

【问题讨论】:

  • 您说您将子模块视为实现细节。这不是子模块背后的设计意图;它们可以并且非常普遍地是包的公共接口的一部分。看collectionsnumpyrequeststensorflowurllibasyncio
  • @user2357112supportsMonica 我知道这一点。但同样可以通过显式导入子模块来实现。例如 numpy.linalg:from . import linalg。我只是觉得奇怪的是from .linalg import dotlinalg 绑定到numpy 范围。所以我不认为子模块一般不应该是接口的一部分,但我认为它不应该隐式发生。
  • “但没有真正的动力”。它从字面上完全解释了您的情况,但如果您正在寻找原因,则必须浏览邮件列表

标签: python python-3.x python-import python-packaging


【解决方案1】:

您说您将子模块视为实现细节。这不是子模块背后的设计意图;它们可以并且非常普遍地是包的公共接口的一部分。导入系统旨在促进对子模块的访问,而不是阻止访问。

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

import package.submodule

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

此时,您几乎肯定会想,“嘿,from .submodule import function 没有理由做同样的事情!”它做同样的事情是因为这个属性绑定是子模块初始化的一部分,它只发生在第一次导入时,并且无论哪种导入触发它都需要进行相同的设置。

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

【讨论】:

    【解决方案2】:

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

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

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

    实际上,Python 的设计者认为事情是相反的:“包”(注意在运行时没有“包”类型)主要是为了方便组织相关模块的集合 - 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 `对象。

    现在 wrt/ “不要期望它们成为 API 的一部分,因为结构可以改变”,这当然已经被考虑在内。从单个模块开始,然后随着代码库的增长将其转换为一个包,这实际上是一种非常常见的模式——当然,不会影响客户端代码。这里的关键是正确使用包的初始化程序 - __init__.py 文件 - 这实际上是构建包的 module 实例的基础。这让包充当“门面”,掩盖了哪个子模块有效地定义了哪个函数、类或其他内容的“实现细节”。

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

    FWIW,这最后一个操作也应该对你所有的子模块完成,你也可以使用 _single_leading_underscore 命名约定来处理真正“实现细节”的东西。

    当然,这一切都不会阻止任何人直接从您的子模块导入“私人”名称,但是当出现问题时(“我们都是同意的成年人”等),它们会自行处理。

    【讨论】:

      猜你喜欢
      • 2016-12-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-03
      • 2019-10-27
      • 2018-03-17
      • 1970-01-01
      相关资源
      最近更新 更多