Python中的循环导入依赖

Posted

技术标签:

【中文标题】Python中的循环导入依赖【英文标题】:Circular import dependency in Python 【发布时间】:2010-12-06 02:19:42 【问题描述】:

假设我有以下目录结构:

a\
    __init__.py
    b\
        __init__.py
        c\
            __init__.py
            c_file.py
        d\
            __init__.py
            d_file.py

a 包的__init__.py 中,导入了c 包。但是c_file.py 导入a.b.d

程序失败,当c_file.py 尝试导入a.b.d 时,说b 不存在。 (而且它确实不存在,因为我们正在导入它。)

如何解决这个问题?

【问题讨论】:

也许你可以尝试相对进口? ***.com/questions/72852/… 这可能对ncoghlan_devs-python-notes.readthedocs.org/en/latest/…有帮助 也只是作为参考,python 3.5(可能更高版本)似乎允许循环导入,但 3.4(可能低于)。 如果您发现导入错误,只要您在第一个模块完成导入之前不需要在另一个模块中使用任何东西,它就可以正常工作。 Circular (or cyclic) imports in Python的可能重复 【参考方案1】:

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

def my_function():
    from a.b.c import Blah
    return Blah()

也就是说,将导入推迟到真正需要时。但是,我也会仔细查看我的包定义/使用,因为像所指出的那样的循环依赖可能表明存在设计问题。

【讨论】:

有时循环引用确实是不可避免的。这是在这些情况下唯一适合我的方法。 这不会在每次调用 foo 时增加很多开销吗? @Mr_and_Mrs_D - 只是适度。 Python 将所有导入的模块保存在全局缓存 (sys.modules) 中,因此一旦加载了模块,就不会再次加载它。代码可能涉及在每次调用 my_function 时查找名称,但代码也是如此,它通过限定名称引用符号(例如,import foo; foo.frobnicate() 在所有可能的解决方案中,这是唯一对我有用的解决方案。在某些情况下,循环引用是“最好的”解决方案——尤其是当您正在做的是将一组模型对象拆分为多个文件以限制文件大小时。 有时循环引用正是对问题建模的正确方法。循环依赖在某种程度上表明设计不佳的概念似乎更多地反映了 Python 作为一种语言,而不是合法的设计点。【参考方案2】:

如果a依赖c,c依赖a,那么它们实际上不是同一个单元吗?

您应该真正检查一下为什么将 a 和 c 拆分为两个包,因为要么您有一些代码应该拆分到另一个包中(使它们都依赖于该新包,但不依赖于彼此),或者您应该将它们合并到一个包中。

【讨论】:

是的,它们可以被认为是同一个包。但是,如果这会产生一个巨大的文件,那么这是不切实际的。我同意,循环依赖通常意味着应该重新考虑设计。但是有一些适合的设计模式(并且将文件合并在一起会导致文件很大)所以我认为说应该组合包或重新评估设计是教条的。【参考方案3】:

我曾多次想过这个问题(通常是在与需要相互了解的模型打交道时)。简单的解决方案就是导入整个模块,然后引用你需要的东西。

所以不要这样做

from models import Student

合而为一,

from models import Clas-s-room

在另一个,只是做

import models

其中之一,然后在需要时致电models.Clas-s-room

【讨论】:

你能展示一下models.py的样子吗?我不想将所有类定义放在一个文件中。我想创建一个models.py,从它自己的文件中导入每个类。我需要查看一个示例文件结构。 它不需要是一个文件@ROMS模型可以是一个目录,其中包含一个从models.clas-s-room导入的__init__.py文件。【参考方案4】:

类型提示导致的循环依赖

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

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

没有类型提示,没有错误

文件:vertex.py

class Vertex:
    def __init__(self, label):
        self.label = label
        self.adjacency_list = []

文件:edge.py

class Edge:
    def __init__(self, v1, v2):
        self.v1 = v1
        self.v2 = v2

类型提示导致 ImportError

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

文件:vertex.py

from typing import List
from edge import Edge


class Vertex:
    def __init__(self, label: str):
        self.label = label
        self.adjacency_list: List[Edge] = []

文件:edge.py

from vertex import Vertex


class Edge:
    def __init__(self, v1: Vertex, v2: Vertex):
        self.v1 = v1
        self.v2 = v2

使用 TYPE_CHECKING 的解决方案

文件:vertex.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'] = []

文件: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

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

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

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

from __future__ import annotations

这允许使用不带引号的类型提示和条件导入。

Python 3.10(见PEP 563 -- Postponed Evaluation of Annotations)

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

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

【讨论】:

【参考方案5】:

问题是当从一个目录运行时,默认情况下只有子目录的包作为候选导入可见,所以你不能导入 a.b.d。但是,您可以导入 b.d.因为 b 是 a 的子包。

如果你真的想在c/__init__.py 中导入a.b.d,你可以通过将系统路径更改为a 上方的一个目录并将a/__init__.py 中的导入更改为import a.b.c.

您的a/__init__.py 应如下所示:

import sys
import os
# set sytem path to be directory above so that a can be a 
# package namespace
DIRECTORY_SCRIPT = os.path.dirname(os.path.realpath(__file__)) 
sys.path.insert(0,DIRECTORY_SCRIPT+"/..")
import a.b.c

当您想将 c 中的模块作为脚本运行时,会出现另一个困难。这里包 a 和 b 不存在。您可以破解 c 目录中的 __int__.py 以将 sys.path 指向***目录,然后在 c 内的任何模块中导入 __init__ 以便能够使用完整路径导入 a.b.d。我怀疑导入 __init__.py 是一种好习惯,但它对我的用例有效。

【讨论】:

【参考方案6】:

我建议以下模式。使用它将允许自动完成和类型提示正常工作。

cyclic_import_a.py

import playground.cyclic_import_b

class A(object):
    def __init__(self):
        pass

    def print_a(self):
        print('a')

if __name__ == '__main__':
    a = A()
    a.print_a()

    b = playground.cyclic_import_b.B(a)
    b.print_b()

cyclic_import_b.py

import playground.cyclic_import_a

class B(object):
    def __init__(self, a):
        self.a: playground.cyclic_import_a.A = a

    def print_b(self):
        print('b1-----------------')
        self.a.print_a()
        print('b2-----------------')

您不能使用此语法导入 A 类和 B 类

from playgroud.cyclic_import_a import A
from playground.cyclic_import_b import B

你不能在 B 类的 __init __ 方法中声明参数 a 的类型,但是你可以这样“转换”它:

def __init__(self, a):
    self.a: playground.cyclic_import_a.A = a

【讨论】:

【参考方案7】:

另一种解决方案是为 d_file 使用代理。

例如,假设您想与 c_file 共享 blah 类。因此,d_file 包含:

class blah:
    def __init__(self):
        print("blah")

这是您在 c_file.py 中输入的内容:

# do not import the d_file ! 
# instead, use a place holder for the proxy of d_file
# it will be set by a's __init__.py after imports are done
d_file = None 

def c_blah(): # a function that calls d_file's blah
    d_file.blah()

并且在a的init.py中:

from b.c import c_file
from b.d import d_file

class Proxy(object): # module proxy
    pass
d_file_proxy = Proxy()
# now you need to explicitly list the class(es) exposed by d_file
d_file_proxy.blah = d_file.blah 
# finally, share the proxy with c_file
c_file.d_file = d_file_proxy

# c_file is now able to call d_file.blah
c_file.c_blah() 

【讨论】:

像这样在不同的文件中修改全局模块属性会很快导致一场噩梦

以上是关于Python中的循环导入依赖的主要内容,如果未能解决你的问题,请参考以下文章

如何正确处理 Python 中的循环模块依赖关系?

Python中的循环依赖

Python中的页面对象模式循环导入问题

(转)Python中的模块循环导入问题

在不修改 sys.path 或 3rd 方包的情况下导入 Python 包中的供应商依赖项

CSMS 实现中的循环导入错误