尝试从扁平命名空间导入模块时出现 ModuleNotFoundError

ModuleNotFoundError when attempting to import module from flattened namespace

假设我的项目结构如下:

proj
   ├── __init__.py
   └── core
      ├── __init__.py
      └── a
         ├── __init__.py
         └── foo.py

唯一的非空文件是:

# proj/__init__.py
from . core import a

# proj/core/a/__init__.py
from . foo import Bar

# proj/core/a/foo.py
class Bar:
    pass

我的想法是将模块 acore 中提取出来,并将其暴露在包的顶层。但是,以下失败:

>>> from proj.a import Bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'proj.a'

一样:

>>> import proj.a.Bar as Bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'proj.a'

如果我使用完全限定的模块路径,则以下内容有效:

>>> from proj.core.a import Bar

两个问题:

  1. python 文档中是否有某些部分解释了为什么 import/module 系统不允许失败案例?我没能找到明确的解释。

  2. 是否有变通办法让您可以像从 proj.core.a 一样从 proj.a 导入?

首先让我们先解决一个简单的问题:import proj.a.Bar as Bar 无法工作,因为 Bar 是一个 class,而不是一个模块。出于同样的原因,import proj.core.a.Bar as Bar 也不会奏效。

现在从 proj.a 导入。您描述的使用 __init__.py 文件的 "lifting" 进程只创建了一个别名 - proj.__dict__ 中添加的条目 "a" 现在绑定到一个模块对象,但它本身确实not make 不会使模块 proj.core.a 成为 proj 的直接子模块。

名称 "a" 刚刚绑定在两个不同的 命名空间 中:在 proj.__dict__proj.core.__dict__ 中。 import 语句 from proj.a import Bar 仍然以与往常相同的方式处理,试图找到具有完全限定名称 proj.a 的模块。 Python 将 select 从 sys.meta_path and sys.path_hooks (it will use a PathFinder and a FileFinder), and then search through sys.path 进口机器用于 dir/subdir 像 proj/a,找不到。

  1. Is there some section of the python documentation that explains why the import/module system doesn't allow the failing cases? I haven't been able to find a clear explanation.

我能找到的最好的是在导入系统文档的这一部分 5.3. Searching, where it's mentioned that Python needs the fully qualified name of the module being imported. The failing case doesn't use the fully qualified name,它使用了其他名称。

  1. Is there a work around so that you can import from proj.a as if it were proj.core.a?

是的。在 Python 尝试导入模块之前,它首先检查该键是否已经缓存在 sys.modules 字典中。试试这个解决方法:

# proj/__init__.py
from . core import a

import sys
sys.modules[f"{__name__}.a"] = a

现在 from proj.a import Bar 可以成功,并且 即使 proj 尚未导入 也是如此。

您正在劫持 importlib here, so Python actively enables this trick. There is even a test case for it working in CPython,因为 stdlib xml 和 etree 也依赖于该行为。尽管源代码注释确实称其为 "Crazy",因此请谨慎使用!