【问题标题】:Detecting circular imports检测循环进口
【发布时间】:2026-02-09 09:20:12
【问题描述】:

我正在处理一个包含大约 30 个独特模块的项目。它设计得不太好,因此在向项目添加一些新功能时,我通常会创建循环导入。

当然,当我添加循环导入时,我并没有意识到这一点。有时很明显,当我收到AttributeError: 'module' object has no attribute 'attribute' 之类的错误时,我已经进行了循环导入,其中我明确定义了'attribute'。但其他时候,代码不会因为使用方式而引发异常。

所以,对于我的问题:

是否可以以编程方式检测循环导入发生的时间和地点?

到目前为止,我能想到的唯一解决方案是有一个模块importTracking,其中包含一个字典importingModules,一个函数importInProgress(file),它递增importingModules[file],如果大于1,则抛出错误,和一个函数importComplete(file) 递减importingModules[file]。所有其他模块如下所示:

import importTracking
importTracking.importInProgress(__file__)
#module code goes here.
importTracking.importComplete(__file__)

但这看起来真的很讨厌,一定有更好的方法来做到这一点,对吧?

【问题讨论】:

  • 你怎么知道你创建了循环导入?如果你不知道,有什么问题?请具体说明问题。

标签: python circular-dependency


【解决方案1】:

Python 中的循环导入不像 PHP 包含的那样。

Python 导入的模块第一次加载到导入“处理程序”中,并在整个过程中保留在那里。此处理程序为从该模块导入的任何内容分配本地名称空间中的名称,用于每次后续导入。一个模块是唯一的,对该模块名称的引用将始终指向同一个加载的模块,无论它是从哪里导入的。

因此,如果您有一个循环模块导入,每个文件的加载将发生一次,然后每个模块将具有与创建到其命名空间中的另一个模块相关的名称。

在两个模块中引用特定名称时当然可能会出现问题(当循环导入发生在相对模块的导入中引用的类/函数定义之前),但如果那样的话,你会得到一个错误发生。

【讨论】:

  • 嘿。由于导入通常发生在文件的顶部,因此您在上一段中描述的情况不是通常的情况。所以循环导入通常会抛出一个异常,if 你可以确定你实际上已经导入了一个参与的模块。还是我误会了?
【解决方案2】:

为避免更改每个模块,您可以将导入跟踪功能粘贴在 import hook 或自定义的 __import__ 中,您可以粘贴在内置插件中——后者可能会工作一次更好,因为即使要导入的模块已经在 sys.modules 中,也会调用 __import__,循环导入时就是这种情况。

对于实现,我只需使用“正在导入的过程中”的一组模块,例如(benjaoming 编辑:插入从原始派生的工作 sn-p):

beingimported = set()
originalimport = __import__
def newimport(modulename, *args, **kwargs):
    if modulename in beingimported:
        print "Importing in circles", modulename, args, kwargs
        print "    Import stack trace -> ", beingimported
        # sys.exit(1) # Normally exiting is a bad idea.
    beingimported.add(modulename)
    result = originalimport(modulename, *args, **kwargs)
    if modulename in beingimported:
        beingimported.remove(modulename)
    return result
import __builtin__
__builtin__.__import__ = newimport

【讨论】:

  • 谢谢,但是:这在两种情况下破坏了原始程序:1)class Somethink(object): myimport = __import__ ... self.myimport(name)(self 对象在 modulename 中,真正的模块名在 etc 中,例如在 django.conf 中)` 2 )try: import some_module \n except: some_module = None(保留在集合中,下次报告误报)
  • 如果包 foo_package 有一个模块 "foo_package/a.py" 导入 from foo_package import b,那么即使 "foo_package/__init__.py" 是空的文件。这是错误的。在“from import ”的情况下,重要的模块名称在被忽略的“etc etc”中而不是在modulename中!
  • 我看到的是empty modulename(即,它是一个长度为0的字符串)。
【解决方案3】:

import 使用__builtin__.__import__(),因此如果您使用monkeypatch 进行修改,那么任何地方的每个导入都会获取更改。请注意,循环导入不一定是个问题。

【讨论】:

    【解决方案4】:

    并非所有循环导入都是问题,正如您在未引发异常时发现的那样。

    当它们出现问题时,您将在下次尝试运行任何测试时遇到异常。发生这种情况时,您可以更改代码。

    我认为这种情况不需要任何改变。

    不存在问题的示例:

    a.py

    import b
    a = 42
    def f():
      return b.b
    

    b.py

    import a
    b = 42
    def f():
      return a.a
    

    【讨论】:

    • 嗯,正如我所说,这是一个设计不佳的项目。我有一个包含项目全局设置的模块。我还有其他配置预设设置的模块。这些模块可能触发循环导入,并且不会出现任何错误,但只是不做他们的工作。