【问题标题】:Circular (or cyclic) imports in PythonPython 中的循环(或循环)导入
【发布时间】:2010-10-19 03:58:30
【问题描述】:

如果两个模块相互导入会发生什么?

为了概括这个问题,Python 中的循环导入呢?

【问题讨论】:

  • 也只是作为参考,似乎在 python 3.5(可能更高)上允许循环导入,但在 3.4(可能低于)上不允许。
  • 我正在使用 python 3.7.2,但由于循环依赖,我仍然遇到运行时错误。

标签: python circular-dependency cyclic-reference


【解决方案1】:

去年在comp.lang.python 上对此进行了非常好的讨论。它非常彻底地回答了您的问题。

导入确实非常简单。请记住以下几点:

'import' 和 'from xxx import yyy' 是可执行语句。他们执行 当正在运行的程序到达该行时。

如果模块不在 sys.modules 中,则导入会创建新模块 进入 sys.modules 然后执行模块中的代码。它不是 将控制权返回给调用模块,直到执行完成。

如果 sys.modules 中确实存在一个模块,那么导入只会返回该模块 模块是否已完成执行。这就是为什么 循环导入可能会返回部分为空的模块。

最后,执行脚本运行在一个名为 __main__ 的模块中,导入 以它自己的名字命名的脚本将创建一个与它无关的新模块 __main__.

综合考虑,导入时不会有任何意外 模块。

【讨论】:

  • @meawoppl 你能扩展这个评论吗?它们具体发生了哪些变化?
  • 到目前为止,python3 中唯一对循环导入的引用“What's new?”页面是in the 3.5 one。它说“现在支持涉及相对进口的循环进口”。 @meawoppl 您还发现了这些页面中未列出的其他内容吗?
  • 他们是默认的。 3.0-3.4 不支持。或者至少成功的语义是不同的。这是我发现没有提及 3.5 更改的概要。 gist.github.com/datagrok/40bf84d5870c41a77dc6
  • 请你扩展这个“最后,执行脚本运行在一个名为main的模块中,以自己的名字导入脚本将创建一个与主要。”。因此,假设该文件是 a.py 并且当它作为主入口点运行时,它的 main 现在如果它具有来自导入某个变量的代码。那么同一个文件'a.py'会被加载到系统模块表中吗?那么这是否意味着如果它有说打印语句,那么它将运行两次?一次用于主文件,再次在遇到导入时?
  • 这个答案已有 10 年历史,我想要一个现代化的更新,以确保它在各种版本的 Python、2.x 或 3.x 中保持正确
【解决方案2】:

循环导入终止,但您需要注意不要在模块初始化期间使用循环导入的模块。

考虑以下文件:

a.py:

print "a in"
import sys
print "b imported: %s" % ("b" in sys.modules, )
import b
print "a out"

b.py:

print "b in"
import a
print "b out"
x = 3

如果你执行a.py,你会得到如下:

$ python a.py
a in
b imported: False
b in
a in
b imported: True
a out
b out
a out

在第二次导入 b.py 时(在第二次 a in 中),Python 解释器不会再次导入 b,因为它已经存在于模块 dict 中。

如果您在模块初始化期间尝试从a 访问b.x,您将获得AttributeError

将以下行追加到a.py:

print b.x

那么,输出是:

$ python a.py
a in                    
b imported: False
b in
a in
b imported: True
a out
Traceback (most recent call last):
  File "a.py", line 4, in <module>
    import b
  File "/home/shlomme/tmp/x/b.py", line 2, in <module>
    import a
 File "/home/shlomme/tmp/x/a.py", line 7, in <module>
    print b.x
AttributeError: 'module' object has no attribute 'x'

这是因为模块在导入时执行,在访问b.x 时,x = 3 行尚未执行,这只会在b out 之后发生。

【讨论】:

  • 这很好地解释了问题,但是解决方案呢?我们如何正确导入和打印 x?上面的其他解决方案对我不起作用
  • 我认为如果您使用__name__ 而不是'a',这个答案会受益匪浅。一开始我很困惑为什么一个文件会被执行两次。
  • @mehmet 重构您的项目,使导入语句形成树状结构(主脚本导入支持模块,这些模块本身可以导入其支持模块等)。这是通常可取的方法。
【解决方案3】:

如果您执行import foo(在bar.py 内)和import bar(在foo.py 内),它将正常工作。当任何东西实际运行时,两个模块都将完全加载并相互引用。

问题是当您改为使用from foo import abc(在bar.py 内)和from bar import xyz(在foo.py 内)时。因为现在每个模块都需要另一个模块已经被导入(这样我们正在导入的名称就存在)才能被导入。

【讨论】:

  • 看来from foo import *from bar import * 也可以正常工作。
  • 使用 a.py/b.py 检查对上述帖子的编辑。他不使用from x import y,但仍然收到循环导入错误
  • 这并不完全正确。就像 import * from 一样,如果您尝试在顶层访问循环导入中的元素,那么在脚本完成运行之前,您将遇到同样的问题。例如,如果您在一个包中从另一个包中设置一个全局包,并且它们都相互包含。我这样做是为了为基类中的对象创建一个草率的工厂,其中该对象可能是多个子类之一,并且使用代码不需要知道它实际创建的是哪个。
  • @Akavall 不是。这只会导入在执行import 语句时可用的名称。所以它不会出错,但你可能无法得到你期望的所有变量。
  • 注意,如果你做from foo import *from bar import *,在foo中执行的一切都处于bar的初始化阶段,而bar中的实际功能尚未完成定义...
【解决方案4】:

这里有一个让我印象深刻的例子!

foo.py

import bar

class gX(object):
    g = 10

bar.py

from foo import gX

o = gX()

ma​​in.py

import foo
import bar

print "all done"

在命令行: $ python main.py

Traceback (most recent call last):
  File "m.py", line 1, in <module>
    import foo
  File "/home/xolve/foo.py", line 1, in <module>
    import bar
  File "/home/xolve/bar.py", line 1, in <module>
    from foo import gX
ImportError: cannot import name gX

【讨论】:

  • 你是如何解决这个问题的?我正在尝试了解循环导入以解决我自己的一个看起来非常与您正在做的事情相似的问题...
  • Erm... 我想我用这个非常丑陋的黑客解决了我的问题。 {{{ if not 'foo.bar' in sys.modules: from foo import bar else: bar = sys.modules['foo.bar'] }}} 就个人而言,我认为循环导入是错误代码的一个巨大警告信号设计...
  • @c089,或者您可以将import bar 中的foo.py 移到末尾
  • 如果barfoo 都必须使用gX,“最干净”的解决方案是将gX 放在另一个模块中,并同时让foobar 导入该模块. (在没有隐藏语义依赖的意义上最干净。)
  • 蒂姆有一个好点子。基本上这是因为bar 在 foo 中甚至找不到gX。循环导入本身没问题,只是gX在导入时没有定义。
【解决方案5】:

好的,我想我有一个很酷的解决方案。 假设您有文件a 和文件b。 你有一个def 或一个class 在文件b 中你想在模块a 中使用,但你还有别的东西,要么是defclass,要么是来自文件a 的变量您需要在文件b 中的定义或类中。 您可以做的是,在文件a 的底部,在调用文件a 中需要的文件b 中的函数或类之后,但在从文件b 中调用您需要的函数或类之前对于文件a,比如import b 然后,这里是关键部分,在文件b 中需要文件a 中的defclass 的所有定义或类中(我们称之为CLASS ),你说from a import CLASS

这是可行的,因为您可以导入文件 b,而无需 Python 执行文件 b 中的任何导入语句,因此您可以避开任何循环导入。

例如:

文件a:

class A(object):

     def __init__(self, name):

         self.name = name

CLASS = A("me")

import b

go = B(6)

go.dostuff

文件 b:

class B(object):

     def __init__(self, number):

         self.number = number

     def dostuff(self):

         from a import CLASS

         print "Hello " + CLASS.name + ", " + str(number) + " is an interesting number."

瞧。

【讨论】:

  • from a import CLASS 实际上并没有跳过执行 a.py 中的所有代码。这是真正发生的事情: (1) a.py 中的所有代码都作为特殊模块“__main__”运行。 (2) 在import b,b.py 中的*代码开始运行(定义B 类),然后控制权返回“__main__”。 (3) "__main__" 最终将控制权交给go.dostuff()。 (4) 当dostuff() 到达import a 时,它再次 运行a.py 中的所有代码,这次作为模块“a”;然后它从新模块“a”中导入 CLASS 对象。所以实际上,如果你在 b.py 中的任何地方使用import a,这将同样有效。
【解决方案6】:

正如其他答案描述的那样,这种模式在 python 中是可以接受的:

def dostuff(self):
     from foo import bar
     ...

这将避免在文件被其他模块导入时执行导入语句。只有存在逻辑循环依赖,才会失败。

大多数循环导入实际上不是逻辑循环导入,而是引发 ImportError 错误,因为 import() 在调用时评估整个文件的*语句的方式。

如果您确实希望将导入放在首位,这些ImportErrors 几乎总是可以避免的

考虑这个循环导入:

应用 A

# profiles/serializers.py

from images.serializers import SimplifiedImageSerializer

class SimplifiedProfileSerializer(serializers.Serializer):
    name = serializers.CharField()

class ProfileSerializer(SimplifiedProfileSerializer):
    recent_images = SimplifiedImageSerializer(many=True)

应用 B

# images/serializers.py

from profiles.serializers import SimplifiedProfileSerializer

class SimplifiedImageSerializer(serializers.Serializer):
    title = serializers.CharField()

class ImageSerializer(SimplifiedImageSerializer):
    profile = SimplifiedProfileSerializer()

来自 David Beazley 的精彩演讲 Modules and Packages: Live and Let Die! - PyCon 20151:54:00,这是一种在 python 中处理循环导入的方法:

try:
    from images.serializers import SimplifiedImageSerializer
except ImportError:
    import sys
    SimplifiedImageSerializer = sys.modules[__package__ + '.SimplifiedImageSerializer']

这会尝试导入SimplifiedImageSerializer,如果引发ImportError,因为它已经被导入,它将从导入缓存中拉取。

PS:你必须用 David Beazley 的声音阅读整篇文章。

【讨论】:

  • 如果模块已经被导入,则不会引发 ImportError。模块可以根据需要多次导入,即“import a; import a;”没问题。
  • 这将使它成为module,而不是我的实验中的class
【解决方案7】:

我完全同意 pythoneer 在这里的回答。但是我偶然发现了一些循环导入存在缺陷的代码,并在尝试添加单元测试时引起了问题。因此,要在不更改所有内容的情况下快速修补它,您可以通过动态导入来解决问题。

# Hack to import something without circular import issue
def load_module(name):
    """Load module using imp.find_module"""
    names = name.split(".")
    path = None
    for name in names:
        f, path, info = imp.find_module(name, path)
        path = [path]
    return imp.load_module(name, f, path[0], info)
constants = load_module("app.constants")

再次重申,这不是永久性修复,但可能会帮助想要修复导入错误而不更改太多代码的人。

干杯!

【讨论】:

    【解决方案8】:

    循环导入可能会造成混淆,因为导入会做两件事:

    1. 它执行导入的模块代码
    2. 将导入模块添加到导入模块全局符号表中

    前者只执行一次,而后者在每个导入语句中执行。循环导入会在导入模块使用导入的部分执行代码时产生情况。因此它不会看到在 import 语句之后创建的对象。下面的代码示例演示了它。

    循环进口并不是要不惜一切代价避免的终极邪恶。在一些像 Flask 这样的框架中,它们是非常自然的,并且调整你的代码以消除它们并不会使代码变得更好。

    main.py

    print 'import b'
    import b
    print 'a in globals() {}'.format('a' in globals())
    print 'import a'
    import a
    print 'a in globals() {}'.format('a' in globals())
    if __name__ == '__main__':
        print 'imports done'
        print 'b has y {}, a is b.a {}'.format(hasattr(b, 'y'), a is b.a)
    

    b.by

    print "b in, __name__ = {}".format(__name__)
    x = 3
    print 'b imports a'
    import a
    y = 5
    print "b out"
    

    a.py

    print 'a in, __name__ = {}'.format(__name__)
    print 'a imports b'
    import b
    print 'b has x {}'.format(hasattr(b, 'x'))
    print 'b has y {}'.format(hasattr(b, 'y'))
    print "a out"
    

    使用 cmets 输出 python main.py

    import b
    b in, __name__ = b    # b code execution started
    b imports a
    a in, __name__ = a    # a code execution started
    a imports b           # b code execution is already in progress
    b has x True
    b has y False         # b defines y after a import,
    a out
    b out
    a in globals() False  # import only adds a to main global symbol table 
    import a
    a in globals() True
    imports done
    b has y True, a is b.a True # all b objects are available
    

    【讨论】:

      【解决方案9】:

      模块 a.py:

      import b
      print("This is from module a")
      

      模块 b.py

      import a
      print("This is from module b")
      

      运行“Module a”会输出:

      >>> 
      'This is from module a'
      'This is from module b'
      'This is from module a'
      >>> 
      

      它输出这 3 行,而由于循环导入,它应该输出 infinitial。 此处列出了运行“Module a”时逐行发生的情况:

      1. 第一行是import b。所以它会访问模块 b
      2. 模块 b 的第一行是import a。所以它会访问模块a
      3. 模块 a 的第一行是 import b请注意,此行将不再执行,因为 python 中的每个文件只执行一个导入行这一次,无论何时何地执行都无关紧要。所以它将传递到下一行并打印"This is from module a"
      4. 从模块 b 访问完整个模块 a 后,我们仍然在模块 b。所以下一行将打印"This is from module b"
      5. 模块 b 行已完全执行。所以我们将回到我们开始模块 b 的模块 a。
      6. import b 行已经执行,不会再执行。下一行将打印"This is from module a",程序将完成。

      【讨论】:

      • 这可能只是因为a.py,当作为脚本执行时,将被命名为“模块__main__”,不是“模块a"。所以当它到达b 并遇到import a 时,现在它会在不同的模块名称 下导入相同的文件,对吧?当__main__ 脚本都不是时会发生什么?
      【解决方案10】:

      我通过以下方式解决了问题,并且运行良好,没有任何错误。 考虑两个文件a.pyb.py

      我将此添加到 a.py 并且它有效。

      if __name__ == "__main__":
              main ()
      

      a.py:

      import b
      y = 2
      def main():
          print ("a out")
          print (b.x)
      
      if __name__ == "__main__":
          main ()
      

      b.py:

      import a
      print ("b out")
      x = 3 + a.y
      

      我得到的输出是

      >>> b out 
      >>> a out 
      >>> 5
      

      【讨论】:

        【解决方案11】:

        这里有很多很棒的答案。虽然问题通常有快速的解决方案,其中一些感觉比其他的更 Pythonic,如果你有做一些重构的奢侈,另一种方法是分析你的代码的组织,并尝试删除循环依赖。例如,您可能会发现:

        文件 a.py

        from b import B
        
        class A:
            @staticmethod
            def save_result(result):
                print('save the result')
        
            @staticmethod
            def do_something_a_ish(param):
                A.save_result(A.use_param_like_a_would(param))
        
            @staticmethod
            def do_something_related_to_b(param):
                B.do_something_b_ish(param)
        

        文件 b.py

        from a import A
        
        class B:
            @staticmethod
            def do_something_b_ish(param):
                A.save_result(B.use_param_like_b_would(param))
        

        在这种情况下,只需将一个静态方法移动到一个单独的文件中,比如c.py

        文件 c.py

        def save_result(result):
            print('save the result')
        

        将允许从 A 中删除 save_result 方法,从而允许从 b 中的 a 中删除 A 的导入:

        重构文件 a.py

        from b import B
        from c import save_result
        
        class A:
            @staticmethod
            def do_something_a_ish(param):
                A.save_result(A.use_param_like_a_would(param))
        
            @staticmethod
            def do_something_related_to_b(param):
                B.do_something_b_ish(param)
        

        重构文件 b.py

        from c import save_result
        
        class B:
            @staticmethod
            def do_something_b_ish(param):
                save_result(B.use_param_like_b_would(param))
        

        总而言之,如果您有一个工具(例如 pylint 或 PyCharm)报告可以是静态的方法,那么仅仅在它们上扔一个 staticmethod 装饰器可能不是消除警告的最佳方法。尽管该方法看起来与类相关,但最好将其分开,特别是如果您有几个密切相关的模块可能需要相同的功能并且您打算练习 DRY 原则。

        【讨论】:

          【解决方案12】:

          假设您正在运行一个名为 request.py 的测试 python 文件 在 request.py 中,你写

          import request
          

          所以这也很可能是循环导入。

          解决方案:

          只需将您的测试文件更改为其他名称,例如aaa.py,而不是request.py

          不要使用其他库已经使用的名称。

          【讨论】:

          • 这是我的最佳答案,因为我的问题只是我将文件命名为类似于我从中导入的库的名称。
          【解决方案13】:

          令我惊讶的是,还没有人提到由类型提示引起的循环导入。
          如果由于类型提示而有循环导入,则可以以干净的方式避免它们。

          考虑main.py,它利用了另一个文件中的异常:

          from src.exceptions import SpecificException
          
          class Foo:
              def __init__(self, attrib: int):
                  self.attrib = attrib
          
          raise SpecificException(Foo(5))
          

          还有专用的异常类exceptions.py

          from src.main import Foo
          
          class SpecificException(Exception):
              def __init__(self, cause: Foo):
                  self.cause = cause
          
              def __str__(self):
                  return f'Expected 3 but got {self.cause.attrib}.'
          

          这将通过 main.py 导入 exception.py 并通过 FooSpecificException 轻松提升 ImportError

          因为Foo 在类型检查期间仅在exceptions.py 中是必需的,所以我们可以使用typing 模块中的TYPE_CHECKING 常量安全地使其有条件导入。类型检查时常量只有True,这样我们可以有条件地导入Foo,从而避免循环导入错误。
          在 Python 3.6 中,使用前向引用:

          from typing import TYPE_CHECKING
          if TYPE_CHECKING:  # Only imports the below statements during type checking
             ​from src.main import Foo
          
          class SpecificException(Exception):
             ​def __init__(self, cause: 'Foo'):  # The quotes make Foo a forward reference
                 ​self.cause = cause
          
             ​def __str__(self):
                 ​return f'Expected 3 but got {self.cause.attrib}.'
          

          在 Python 3.7+ 中,延迟评估注释(在 PEP 563 中引入)允许使用“普通”类型而不是前向引用:

          from __future__ import annotations
          from typing import TYPE_CHECKING
          if TYPE_CHECKING:  # Only imports the below statements during type checking
             ​from src.main import Foo
          
          class SpecificException(Exception):
             ​def __init__(self, cause: Foo):  # Foo can be used in type hints without issue
                 ​self.cause = cause
          
             ​def __str__(self):
                 ​return f'Expected 3 but got {self.cause.attrib}.'
          

          在 Python 3.11+ 中,from __future__ import annotations 默认处于活动状态,因此可以省略。

          此答案基于 Stefaan Lippens 的 Yet another solution to dig you out of a circular import hole in Python

          【讨论】:

            最近更新 更多