Python中的循环导入依赖项

Ram*_*hum 72 python dependencies circular-dependency python-import

假设我有以下目录结构:

a\
    __init__.py
    b\
        __init__.py
        c\
            __init__.py
            c_file.py
        d\
            __init__.py
            d_file.py
Run Code Online (Sandbox Code Playgroud)

a包中__init__.py,c导入包.但是c_file.py进口a.b.d.

程序失败,说尝试导入b时不存在.(它确实不存在,因为我们正在进口它.)c_file.pya.b.d

如何解决这个问题呢?

Dir*_*irk 152

您可以推迟导入,例如a/__init__.py:

def my_function():
    from a.b.c import Blah
    return Blah()
Run Code Online (Sandbox Code Playgroud)

也就是说,将导入推迟到真正需要之前.但是,我也会仔细查看我的包定义/用法,因为像指出的那样循环依赖可能表示设计问题.

  • 有时,循环引用正是模拟问题的正确方法.循环依赖关系在某种程度上表明设计不佳的观点似乎更多地反映了Python作为一种语言而不是合法的设计点. (12认同)
  • @Mr_and_Mrs_D - 只适度.Python将所有导入的模块保存在全局缓存中(`sys.modules`),因此一旦加载了模块,就不会再次加载它.代码可能涉及在每次调用`my_function`时查找名称,但代码也是如此,它通过限定名称引用符号(例如,`import foo; foo.frobnicate()`) (6认同)
  • 有时循环引用确实是不可避免的.在这种情况下,这是唯一适用于我的方法. (4认同)

ang*_*son 61

如果一个取决于c和c取决于a,那么它们实际上不是同一个单位吗?

你应该仔细研究为什么你将a和c分成两个包,因为要么你有一些代码你应该拆分成另一个包(使它们都依赖于那个新的包,而不是彼此),或者你应该合并它们成一个包.

  • 是的,他们可以被认为是同一个包.但如果这导致了一个巨大的文件,那么它是不切实际的.我同意经常,循环依赖意味着应该再次考虑设计.但是有一些设计模式适合(并且将文件合并在一起会产生一个巨大的文件)所以我认为应该合并软件包或者应该重新评估设计是教条主义. (99认同)

zac*_*san 26

我想知道这几次(通常在处理需要彼此了解的模型时).简单的解决方案就是导入整个模块,然后引用您需要的东西.

所以不要这样做

from models import Student
Run Code Online (Sandbox Code Playgroud)

在一个,和

from models import Classroom
Run Code Online (Sandbox Code Playgroud)

在另一方面,就这样做

import models
Run Code Online (Sandbox Code Playgroud)

在其中一个,然后调用models.Classroom当你需要它.

  • 请注意,如果没有立即运行并尝试访问“models”属性的“顶级代码”,**仅**可以解决问题。 (2认同)

Chr*_*ert 9

类型提示导致的循环依赖

使用类型提示,有更多机会创建循环导入。幸运的是,有一个使用特殊常量的解决方案:typing.TYPE_CHECKING.

以下示例定义了一个Vertex类和一个Edge类。一条边由两个顶点定义,一个顶点维护它所属的相邻边的列表。

没有类型提示,没有错误

文件:顶点.py

class Vertex:
    def __init__(self, label):
        self.label = label
        self.adjacency_list = []
Run Code Online (Sandbox Code Playgroud)

文件:edge.py

class Edge:
    def __init__(self, v1, v2):
        self.v1 = v1
        self.v2 = v2
Run Code Online (Sandbox Code Playgroud)

类型提示导致导入错误

导入错误:无法从部分初始化的模块“边缘”导入名称“边缘”(很可能是由于循环导入)

文件:顶点.py

from typing import List
from edge import Edge


class Vertex:
    def __init__(self, label: str):
        self.label = label
        self.adjacency_list: List[Edge] = []
Run Code Online (Sandbox Code Playgroud)

文件:edge.py

from vertex import Vertex


class Edge:
    def __init__(self, v1: Vertex, v2: Vertex):
        self.v1 = v1
        self.v2 = v2
Run Code Online (Sandbox Code Playgroud)

使用 TYPE_CHECKING 的解决方案

文件:顶点.py

from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
    from edge import Edge


class Vertex:
    def __init__(self, label: str):
        self.label = label
        self.adjacency_list: List['Edge'] = []
Run Code Online (Sandbox Code Playgroud)

文件:edge.py

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from vertex import Vertex


class Edge:
    def __init__(self, v1: 'Vertex', v2: 'Vertex'):
        self.v1 = v1
        self.v2 = v2
Run Code Online (Sandbox Code Playgroud)

带引号与不带引号的类型提示

在 Python 3.10 之前的版本中,有条件导入的类型必须用引号括起来,使它们成为“前向引用”,从而将它们隐藏在解释器运行时之外。

在 Python 3.7、3.8 和 3.9 中,解决方法是使用以下特殊导入。

from __future__ import annotations
Run Code Online (Sandbox Code Playgroud)

这允许使用不带引号的类型提示与条件导入相结合。

Python 3.10(参见PEP 563 - 注释的推迟评估

在 Python 3.10 中,将不再在定义时评估函数和变量注释。相反,字符串形式将保留在相应的注释字典中。静态类型检查器在行为上没有区别,而在运行时使用注释的工具将不得不执行延迟评估。

字符串形式是在编译步骤期间从 AST 获得的,这意味着字符串形式可能不会保留源的确切格式。注意:如果一个注解已经是一个字符串文字,它仍然会被包裹在一个字符串中。