Python中的循环导入依赖


77

假设我具有以下目录结构:

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

程序失败,表示尝试导入b时不存在。(它实际上不存在,因为我们正在导入它。)c_file.pya.b.d

如何解决这个问题?


1
也许您可以尝试相对进口? stackoverflow.com/questions/72852/...
eremzeit


同样作为参考,似乎在python 3.5(可能还有更高版本)上允许循环导入,但在3.4(可能是波纹管)上不允许。
查理·帕克

1
如果遇到导入错误,只要在第一个模块完成导入之前不需要在另一个模块中使用任何东西,它就可以正常工作。
Gavin S. Yancey

Answers:


63

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

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


111
是的,可以将它们视为同一软件包。但是,如果这导致文件很大,则不切实际。我同意,循环依赖经常意味着应该重新考虑设计。但是,有些设计模式是适当的(将文件合并在一起会导致文件很大),因此我认为说应该打包或重新评估设计包是教条。
马修·隆德

156

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

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

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


4
有时,循环引用确实是不可避免的。在这种情况下,这是唯一对我有用的方法。
杰森·波利特

1
这不会在foo的每次调用中增加很多开销吗?
Mr_and_Mrs_D 2014年

6
@Mr_and_Mrs_D-仅适度。Python将所有导入的模块都保留在全局缓存(sys.modules)中,因此一旦加载了模块,就不会再次加载。该代码可能涉及名称查找在每次调用my_function,而是通过限定名也是如此代码,参考符号(例如,import foo; foo.frobnicate()
德克

在这里所有可能的解决方案中,这是唯一对我有用的解决方案。在绝对情况下,循环引用是“最佳”解决方案-尤其是当您正在做的事情是在多个文件中拆分一组模型对象以限制文件大小时。
理查德J

14
有时,循环引用恰好是建模问题的正确方法。循环依赖某种程度上表明不良设计的想法似乎更多地反映了Python作为一种语言,而不是合法的设计点。
朱莉在奥斯丁

29

我想知道几次(通常是在处理需要相互了解的模型时)。简单的解决方案是导入整个模块,然后引用所需的内容。

所以不要做

from models import Student

合而为一

from models import Classroom

在另一种,只是做

import models

在其中之一中,然后在需要时调用model.Classroom。


3

类型提示引起的循环依赖

有了类型提示,就有更多的机会创建循环导入。幸运的是,有一个使用特殊常量的解决方案: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-批注的延迟评估

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

字符串形式是在编译步骤中从AST获得的,这意味着字符串形式可能不会保留源的确切格式。注意:如果注释已经是字符串文字,则仍将其包装在字符串中。


0

问题是,从目录运行时,默认情况下只有候选目录的软件包可见,因此您无法导入abd。但是,由于b是a的子软件包,因此可以导入bd。

如果您确实要导入abd,则c/__init__.py可以通过将系统路径更改为a上方的一个目录,并将导入方式更改a/__init__.py为import abc来完成此操作。

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不存在。您可以修改__int__.pyc目录中的,以将sys.path指向顶级目录,然后导入__init__c中的任何模块,以便能够使用完整路径来导入abd。我怀疑导入是一个好习惯,__init__.py但是为我的用例工作。


0

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

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

-4

另一种解决方案是对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()

在一个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() 

11
修改全局模块属性到另一个不同的文件中,将很快导致一场噩梦
锑2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.