包内导入并不总是有效

Intra-package imports do not always work

我有一个结构如下的 Django 项目:

appname/
   models/
      __init__.py
      a.py
      base.py
      c.py

... 其中 appname/models/__init__.py 仅包含如下语句:

from appname.models.base import Base
from appname.models.a import A
from appname.models.c import C

... 并且 appname/models/base.py 包含:

import django.db.models


class Base(django.db.models.Model):
   ...

并且 appname/models/a.py 包含:

import appname.models as models


class A(models.Base):
   ....

...appname/models/c.py 等类似..

我对我的代码的这种结构很满意,但当然它不起作用,因为循环导入。

当appname/__init__.py为运行时,appname/models/a.py会得到运行,但是那个模块导入了"appname.models",还没有执行完然而。经典循环导入。

所以这应该表明我的代码结构不佳,需要重新设计以避免循环依赖。

有哪些选择?

我能想到的一些解决方案以及为什么我不想使用它们:

  1. 将我所有的模型代码合并到一个文件中:在我看来,在同一个文件中包含 20 多个 classes 的风格比我尝试做的(使用单独的文件)要糟糕得多。
  2. 将 "Base" 模型 class 移动到 "appname/models" 之外的另一个包中:这意味着我最终会在我的项目中包含包含 base/parent classes,理想情况下应该将其拆分到它们的 child/sub classes 所在的包中。为什么我应该在同一个包中而不是在它们自己的包中包含用于模型、表单、视图等的 base/parent classes(其中 child/sub classes 应该是位于),而不是为了避免循环导入?

所以我的问题不仅仅是如何避免循环导入,而是以一种与我试图实现的一样干净(如果不是更干净的话)的方式来做到这一点。

有没有人有更好的方法?

编辑

我对此进行了更彻底的研究,得出的结论是这是核心 Python 或 Python 文档中的错误。更多信息可用 at this question and answer

Python 的 PEP 8 表明明显偏好绝对导入而不是相对导入。这个问题有一个涉及相对导入的解决方法,并且在导入机制中有一个可能的修复。

下面我的原始答案给出了示例和解决方法。

原回答

正如您正确推断的那样,问题是循环依赖。在某些情况下,Python 可以很好地处理这些问题,但如果嵌套导入过多,就会出现问题。

例如,如果你只有一个包级别,实际上很难打破它(没有相互导入),但是一旦你嵌套包,它的工作方式更像是相互导入,它开始变得难以让它发挥作用。这是一个引发错误的示例:

level1/__init__.py

    from level1.level2 import Base

level1/level2/__init__.py

    from level1.level2.base import Base
    from level1.level2.a import A

level1/level2/a.py

    import level1.level2.base
    class A(level1.level2.base.Base): pass

level1/level2/base

    class Base: pass

错误可能 "fixed"(对于这个小案例)有几种不同的方式,但许多潜在的修复是脆弱的。例如,如果您不需要在 level2 __init__ 文件中导入 A,删除该导入将解决问题(并且您的程序稍后可以执行 import level1.level2.a.A),但是如果你的包变得更复杂,你会看到错误再次出现。

Python 有时可以很好地使这些复杂的导入工作,并且它们何时工作和不工作的规则一点也不直观。一个通用规则是 from xxx.yyy import zzzimport xxx.yyy 后跟 xxx.yyy.zzz 更宽容。在后一种情况下,解释器必须在检索 xxx.yyy.zzz 时完成将 yyy 绑定到 xxx 命名空间,但在前一种情况下,解释器可以遍历模块顶级包命名空间之前的包已完全设置。

所以对于这个例子,真正的问题是 a.py 中的裸导入 这很容易解决:

    from level1.level2.base import Base
    class A(Base): pass

始终使用相对导入是强制使用 from ... import 的好方法,原因很简单,如果没有 from'. To use relative imports with the example above,level1/level2/a.py` 应该包含,相对导入就无法工作:

from .base import Base
class A(Base): pass

这打破了有问题的导入周期,其他一切正常。如果导入的名称(例如 Base)在没有以源模块名称为前缀时过于笼统而容易混淆,您可以在导入时轻松重命名它:

from .base import Base as BaseModel
class A(BaseModel): pass

虽然这解决了当前的问题,但如果包结构变得更加复杂,您可能需要考虑更普遍地使用相对导入。例如,level1/level2/__init__.py 可以是:

from .base import Base
from .a import A