为什么循环导入在调用栈上层看似有效,但在下层则引发ImportError?

157 投票
8 回答
188557 浏览
提问于 2025-04-17 20:52

我遇到了这个错误

Traceback (most recent call last):
  File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module>
    from world import World
  File "/Users/alex/dev/runswift/utils/sim2014/world.py", line 2, in <module>
    from entities.field import Field
  File "/Users/alex/dev/runswift/utils/sim2014/entities/field.py", line 2, in <module>
    from entities.goal import Goal
  File "/Users/alex/dev/runswift/utils/sim2014/entities/goal.py", line 2, in <module>
    from entities.post import Post
  File "/Users/alex/dev/runswift/utils/sim2014/entities/post.py", line 4, in <module>
    from physics import PostBody
  File "/Users/alex/dev/runswift/utils/sim2014/physics.py", line 21, in <module>
    from entities.post import Post
ImportError: cannot import name Post

你可以看到我在上面用的导入语句是一样的,而且它是可以工作的。是不是有什么不成文的规则关于循环导入?我该如何在调用栈的后面使用同一个类呢?


另外,查看 在Python中使用互相或循环导入会发生什么? 了解哪些是允许的,哪些会导致循环导入的问题。还有 我该如何处理“ImportError: Cannot import name X”或“AttributeError: ...(很可能是由于循环导入)”? 了解解决和避免循环依赖的技巧。

8 个回答

10

我可以在需要这个模块里面对象的函数中导入这个模块(仅限于这个函数)。

def my_func():
    import Foo
    foo_instance = Foo()
25

对于那些像我一样从Django入手的朋友们,你们应该知道,文档里提供了解决方案:https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

“...如果你想引用在另一个应用中定义的模型,你可以明确地指定一个模型,使用完整的应用标签。例如,如果上面的Manufacturer模型是在另一个叫做production的应用中定义的,你需要这样写:

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',
        on_delete=models.CASCADE,
)

这种引用方式在解决两个应用之间的循环导入依赖时非常有用。...”

67

要理解循环依赖,首先要知道Python其实是一种脚本语言。在方法外的语句执行是在编译时进行的。导入语句的执行方式就像调用方法一样,所以你可以把它们想象成方法调用。

当你进行导入时,发生的事情取决于你要导入的文件是否已经存在于模块表中。如果存在,Python就会使用当前的符号表。如果不存在,Python会开始读取模块文件,编译、执行并导入它找到的内容。在编译时引用的符号是否能找到,取决于编译器是否已经见过这些符号。

想象一下你有两个源文件:

文件 X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

文件 Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

现在假设你编译文件 X.py。编译器首先定义方法 X1,然后遇到 X.py 中的导入语句。这时,编译器暂停对 X.py 的编译,开始编译 Y.py。没过多久,编译器在 Y.py 中遇到导入语句。因为 X.py 已经在模块表中,Python 就会使用现有的、不完整的 X.py 符号表来满足任何请求的引用。在 X.py 中导入语句之前的符号现在在符号表中,但导入语句之后的符号则不在。由于 X1 现在出现在导入语句之前,它成功被导入。然后,Python 继续编译 Y.py,定义 Y2,并完成 Y.py 的编译。接着,它恢复对 X.py 的编译,并在 Y.py 符号表中找到 Y2。最终编译完成,没有错误。

如果你从命令行编译 Y.py,会发生完全不同的事情。在编译 Y.py 时,编译器在定义 Y2 之前就遇到了导入语句。然后它开始编译 X.py。很快,它在 X.py 中遇到需要 Y2 的导入语句。但 Y2 还没有定义,所以编译失败。

请注意,如果你修改 X.py 以导入 Y1,编译总是会成功,无论你编译哪个文件。然而,如果你修改 Y.py 以导入符号 X2,两个文件都将无法编译。

任何时候,当模块 X 或者 X 导入的任何模块可能会导入当前模块时,请不要使用:

from X import Y

每当你觉得可能存在循环导入时,也应该避免在编译时引用其他模块中的变量。考虑一下看起来无害的代码:

import X
z = X.Y

假设模块 X 在这个模块导入 X 之前就导入了这个模块。再假设 Y 在导入语句之后定义在 X 中。那么在这个模块被导入时,Y 将不会被定义,你会遇到编译错误。如果这个模块先导入 Y,你就可以避免这个问题。但是当你的同事无意中改变第三个模块中的定义顺序时,代码就会出错。

在某些情况下,你可以通过将导入语句移动到其他模块所需的符号定义之后来解决循环依赖。在上面的例子中,导入语句之前的定义从来不会失败。导入语句之后的定义有时会失败,这取决于编译的顺序。你甚至可以把导入语句放在文件的最后,只要在编译时不需要导入的符号。

注意,将导入语句放在模块后面会让你的意图变得不清晰。可以在模块顶部加上注释来解释,比如:

#import X   (actual import moved down to avoid circular dependency)

一般来说,这是一种不好的做法,但有时很难避免。

70

当你第一次导入一个模块(或者模块里的某个成员)时,模块里的代码会像其他代码一样按顺序执行;也就是说,它和函数体的处理方式没有什么不同。import 只是一个普通的命令,就像赋值、调用函数、定义函数(def)或者定义类(class)一样。如果你的导入语句放在脚本的最上面,事情是这样的:

  • 当你尝试从 world 导入 World 时,world 脚本会被执行。
  • 然后,world 脚本又导入了 Field,这会导致 entities.field 脚本被执行。
  • 这个过程会一直进行,直到你到达 entities.post 脚本,因为你试图导入 Post
  • 接着,entities.post 脚本会执行 physics 模块,因为它试图导入 PostBody
  • 最后,physics 又试图从 entities.post 导入 Post
  • 我不确定 entities.post 模块在内存中是否存在,但这并不重要。要么模块不在内存中,要么模块还没有定义 Post 成员,因为它还没有执行完来定义 Post
  • 无论如何,都会出现错误,因为 Post 不存在,无法被导入。

所以,不是说它在调用栈的更高层“工作”。这是一个错误发生的堆栈跟踪,意味着在那个类中尝试导入 Post 时出错。你不应该使用循环导入。顶多,它的好处微乎其微(通常是没有好处),而且会引发像这样的麻烦。这会给任何维护它的开发者带来负担,迫使他们小心翼翼地避免破坏它。你需要重新组织你的模块结构。

258

我觉得jpmc26的回答虽然没有错,但对循环导入的问题说得有点过了。其实,只要设置得当,循环导入是可以正常工作的。

最简单的方法是使用import my_module的写法,而不是from my_module import some_object。前者几乎总是能正常工作,即使my_module里又导入了我们自己。后者只有在my_object已经在my_module中定义的情况下才能工作,而在循环导入的情况下,这种情况可能并不存在。

针对你的情况:试着把entities/post.py改成import physics,然后用physics.PostBody来引用,而不是直接用PostBody。同样,把physics.py改成import entities.post,然后用entities.post.Post来引用,而不是直接用Post

撰写回答