为什么循环导入似乎在调用堆栈的上层起作用,但随后在下面引发 ImportError ?

2024-12-12 08:40:00
admin
原创
161
摘要:问题描述:我收到此错误Traceback (most recent call last): File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module> from wo...

问题描述:

我收到此错误

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

你可以看到我在更上层使用了相同的 import 语句,并且它起作用了。循环导入是否存在一些不成文的规则?如何在调用堆栈的更下层使用同一个类?


另请参阅在 Python 中使用相互或循环导入时会发生什么?,以大致了解允许哪些内容以及导致循环导入问题的原因。请参阅如何处理“ImportError:无法导入名称 X”或“AttributeError:...(很可能是由于循环导入)”?,以了解解决和避免循环依赖的技术。


解决方案 1:

我认为jpmc26 的回答虽然没有,但过于强调循环导入。如果你正确设置它们,它们可以正常工作。

最简单的方法是使用import my_module语法,而不是from my_module import some_object。前者几乎总是有效的,即使my_moduleincluded 会将我们导入回来。后者仅在my_object已在 中定义时才有效my_module,而在循环导入中可能并非如此。

具体到您的情况:尝试改为entities/post.pydo import physics,然后 refer to,physics.PostBody而不是PostBody直接引用。同样,改为physics.pydo import entities.post,然后 use,entities.post.Post而不是直接引用Post

解决方案 2:

首次导入模块(或其成员)时,模块内的代码将像其他代码一样按顺序执行;例如,它与函数体没有任何区别。 就像import其他命令一样(赋值、函数调用、defclass)。假设导入发生在脚本的顶部,那么将发生以下情况:

  • 当您尝试World从导入时worldworld脚本就会被执行。

  • world脚本导入Field,导致entities.field脚本被执行。

  • 此过程持续进行,直到您到达entities.post脚本,因为您尝试导入Post

  • entities.post脚本导致physics模块被执行,因为它试图导入PostBody

  • 最后,physics尝试Post从导入entities.post

  • 我不确定entities.post模块是否已存在于内存中,但这真的不重要。要么模块不在内存中,要么模块还没有成员,Post因为它还没有完成定义Post

  • 无论哪种方式,都会发生错误,因为Post没有导入

所以,不是“在调用堆栈中进一步工作”。这是错误发生位置的堆栈跟踪,这意味着尝试导入Post该类时出错。您不应该使用循环导入。充其量,它的好处微不足道(通常没有好处),并且会导致这样的问题。它会给维护它的任何开发人员带来负担,迫使他们小心翼翼地避免破坏它。重构您的模块组织。

解决方案 3:

要理解循环依赖,您需要记住 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 中的 import 语句。这会导致编译器暂停 X.py 的编译并开始编译 Y.py。此后不久,编译器命中 Y.py 中的 import 语句。由于 X.py 已在模块表中,因此 Python 使用现有的不完整 X.py 符号表来满足所请求的任何引用。X.py 中 import 语句之前出现的任何符号现在都在符号表中,但之后的任何符号都不在。由于 X1 现在出现在 import 语句之前,因此它已成功导入。然后 Python 恢复编译 Y.py。在此过程中,它定义了 Y2 并完成编译 Y.py。然后它恢复编译 X.py,并在 Y.py 符号表中找到 Y2。编译最终完成且无错误。

如果您尝试从命令行编译 Y.py,则会发生非常不同的事情。在编译 Y.py 时,编译器在定义 Y2 之前命中 import 语句。然后它开始编译 X.py。很快它就命中了 X.py 中需要 Y2 的 import 语句。但 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 语句向下移动会掩盖您正在做的事情。请在模块顶部添加注释以弥补这一点,如下所示:

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

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

解决方案 4:

对于像我一样从 Django 遇到此问题的人,您应该知道文档提供了一个解决方案:
https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

“...要引用另一个应用程序中定义的模型,您可以明确指定具有完整应用程序标签的模型。例如,如果上面的制造商模型是在另一个名为 production 的应用程序中定义的,则需要使用:

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

在解决两个应用程序之间的循环导入依赖关系时,这种引用很有用。...

解决方案 5:

我能够(仅)在需要该模块对象的函数内导入该模块:

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

解决方案 6:

如果您在相当复杂的应用程序中遇到此问题,重构所有导入可能会很麻烦。PyCharm 提供了一个快速修复程序,它会自动更改导入符号的所有用法。

在此处输入图片描述

解决方案 7:

根据这个答案,我们可以在块中导入另一个模块的对象(如函数/方法等),而不会发生循环导入错误,例如对于导入another.py模块的简单对象,您可以使用这个:

def get_simple_obj():
    from another import Simple
    return Simple

class Example(get_simple_obj()):
    pass

class NotCircularImportError:
    pass

在这种情况下,another.py模块可以轻松导入NotCircularImportError,而不会出现任何问题。

解决方案 8:

我使用了以下内容:

from module import Foo

foo_instance = Foo()

但为了摆脱circular reference我做了以下事情并且有效:

import module.foo

foo_instance = foo.Foo()
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1579  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1355  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   8  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   9  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用