检测循环导入

28 投票
4 回答
19620 浏览
提问于 2025-04-15 20:11

我正在做一个项目,这个项目大约有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__)

但这样看起来真的很麻烦,肯定还有更好的方法吧?

4 个回答

1

并不是所有的循环导入都会造成问题,就像你发现的那样,当没有抛出异常时,它们其实是没问题的。

但是,当它们确实造成问题时,你在下次运行任何测试时就会遇到异常。这时候你可以修改代码来解决这个问题。

在这种情况下,我认为不需要做任何改变。

下面是一个循环导入不造成问题的例子:

a.py

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

b.py

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

Python 中,循环导入和 PHP 的包含方式是不一样的。

当你第一次导入一个模块时,Python 会把这个模块加载到一个叫做“导入处理器”的地方,并且在整个程序运行期间都保持在那里。这个处理器会为每次导入的模块在本地命名空间中分配名字。也就是说,不管你在哪里导入这个模块,它的名字总是指向同一个加载好的模块。

所以,如果你有循环导入的情况,每个文件只会加载一次,然后每个模块都会在自己的命名空间中创建与另一个模块相关的名字。

当然,当你在两个模块中引用特定的名字时(如果循环导入发生在引用的类或函数定义之前),可能会出现问题。如果发生这种情况,你会收到一个错误提示。

10

为了避免每个模块都要改动,你可以把跟踪导入功能放在一个叫做导入钩子的东西里,或者在一个自定义的__import__函数中,这个函数可以放在内置函数里。后者可能效果更好,因为__import__函数会在模块已经在sys.modules里的情况下被调用,这种情况在循环导入时会发生。

在实现方面,我会简单地用一个集合来记录“正在导入的模块”,类似于下面这个(benjaoming 编辑:插入一个从原始代码中提取的有效代码片段):

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

撰写回答