【问题标题】:Intra-package imports do not always work包内导入并不总是有效
【发布时间】:2015-09-30 01:16:28
【问题描述】:

我有一个结构如下的 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 多个类比我尝试做的(使用单独的文件)要糟糕得多。
  2. 将“Base”模型类移动到“appname/models”之外的另一个包中:这意味着我最终会在我的项目中使用包含基类/父类的包,理想情况下,这些类应该被拆分到它们所在的包中子/子类位于。除了避免循环导入之外,为什么我应该在同一个包中而不是在它们自己的包中(子类/子类所在的位置)中包含模型、表单、视图等的基类/父类?

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

谁有更好的办法?

【问题讨论】:

  • “编码风格”问题是题外话:避免在标题(和问题)中填充此类问题,而只是提出客观问题。
  • 我的问题和风格有关,怎么跑题了?
  • 他并不是说你的问题是题外话。他编辑了你的标题,这样不仔细阅读的人就不会觉得它离题了。 :-)
  • 一个更通用且可能重复的标题如何比我拥有的更好?我们为什么要迎合不仔细阅读的人?
  • 仅从编辑的标题来看,我的问题现在更有可能被标记为重复,即使它与似乎并不关心良好风格的所有其他循环导入问题不同。

标签: python python-import circular-dependency


【解决方案1】:

编辑

我对此进行了更彻底的研究,并得出结论,这是核心 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

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

Python 有时可以很好地完成这些复杂的导入工作,但关于它们何时会起作用和何时不会起作用的规则根本不直观。一个普遍的规则是from xxx.yyy import zzz 可以比import xxx.yyy 更宽容,然后xxx.yyy.zzz。在后一种情况下,解释器必须完成将yyy绑定到xxx命名空间时才能检索xxx.yyy.zzz,但在前一种情况下,解释器可以遍历包中的模块级别包命名空间已完全建立。

所以对于这个例子,真正的问题是在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

【讨论】:

  • 我现在正在尝试并会报告回来,但是有没有办法将相对导入与声明性导入一起使用?
  • @freshquiz -- 你是什么意思?
  • 所以相对导入似乎确实解决了这个问题,但理想情况下,我想避免在非 __init__.py 模块代码中使用“from”样式的导入。相对进口不可能做到这一点吗?
  • @freshquiz -- 不,不可能。当然,这意味着from .xxx import yyy 实际上是 Python 的实现方式 :-)
  • 我很可能最终会采用你的解决方案,但在我这样做之前,我想问你是否认为我最初的尝试比使用“from”风格的相对导入和污染命名空间。你不是说这是我们需要解决的限制吗?
猜你喜欢
  • 2012-01-31
  • 1970-01-01
  • 2018-04-13
  • 1970-01-01
  • 1970-01-01
  • 2012-12-26
  • 1970-01-01
  • 1970-01-01
  • 2012-04-23
相关资源
最近更新 更多