查找Python/Django中循环导入的工具?

51 投票
5 回答
28152 浏览
提问于 2025-04-17 12:07

我有一个Django应用程序,里面有个递归导入的问题,这导致了一些麻烦。因为这个应用程序比较大,我很难找到循环导入的具体原因。

我知道解决方法就是“不要写循环导入”,但问题是我很难确定这个循环导入是从哪里来的,所以如果有个工具能追踪导入的来源,那就太好了。

有没有这样的工具呢?如果没有,我觉得我已经尽量避免循环导入的问题了——比如尽量把导入放到文件的底部,或者把导入放在函数内部,而不是放在文件的顶部等等,但还是遇到问题。我在想有没有什么技巧可以完全避免这些问题。

再详细说一下...

在Django中,当遇到循环导入时,有时会抛出错误,但有时又会默默通过,但是会导致某些模型或字段根本不存在。令人沮丧的是,这种情况通常在一个环境中(比如WSGI服务器)发生,而在另一个环境中(比如命令行)却没有。所以在命令行测试时,像这样是可以工作的:

Foo.objects.filter(bar__name='Test')

但在网页上就会抛出错误:

FieldError: 无法将关键字 'bar__name' 解析为字段。可选项有:...

而且有几个字段明显缺失。

所以这不可能是代码的简单问题,因为它在命令行中确实可以工作,但在网站上却不行

如果有个工具能搞清楚到底发生了什么,那就太好了ImportError可能是最没用的错误信息了。

5 个回答

10

当我遇到导入错误的时候,我通常会倒着推理。我可能会看到类似“无法从 myproject.views 导入 xyz”的错误,尽管 xyz 是存在的。然后我会做两件事:

  • 我会在自己的代码中搜索所有关于 myproject.views 的导入,并在脑海中列出导入它的模块。

  • 接着,我会检查在 views.py 中是否导入了那些匹配的模块。这通常能帮助我找到问题所在。

一个常见的出错地方是 models.py。这个文件通常是你工作中的核心部分。但要确保你的导入是指向 models.py,而不是反过来。所以应该从 views.py 导入 models,而不是从 models 导入 views。

在 urls.py 中,我通常会导入我的视图(这样一旦出错,我能立刻看到错误信息)。但为了避免循环导入错误,你也可以用点路径字符串来引用你的视图。不过这要看你在 urls.py 中做什么。

关于导入位置的一个建议:把导入放在文件的顶部。如果导入分散在各处,你就很难清楚地知道哪个模块导入了什么。把所有导入都放在顶部(并且整理好顺序)可以帮助你更快找到问题。只有在解决特定的循环导入时,才在函数内部导入。

另外,尽量使用绝对导入,而不是相对导入。我指的是“from myproject.views import xyz”,而不是“from views import xyz”。使用绝对导入并且整理导入列表,可以让你的导入更加清晰和整洁。

23

在Django中,循环导入的一个常见原因是模块之间互相引用外键。Django提供了一种方法,可以通过用字符串的形式明确指定模型,并加上完整的应用标签来避免这个问题:

# from myapp import MyAppModel  ## removed circular import

class MyModel(models.Model):
    myfk = models.ForeignKey(
        'myapp.MyAppModel',  ## avoided circular import
        null=True)

查看详细信息:https://docs.djangoproject.com/en/dev/ref/models/fields/#foreignkey

50

导入错误的原因很容易找到,可以通过ImportError异常的回溯信息来查看。

当你查看回溯信息时,你会发现这个模块之前已经被导入过。它的一个导入又导入了其他东西,执行了主代码,然后又导入了第一个模块。由于第一个模块还没有完全初始化(它还停留在导入代码那),所以你会遇到找不到符号的错误。这是有道理的,因为模块的主代码还没有执行到那个点。

在Django中,常见的原因有:

  1. 从完全不同的模块导入子包,

    比如 from mymodule.admin.utils import ...

    这会首先加载 admin/__init__.py,而这个文件可能会导入很多其他的包(比如模型、管理员视图)。管理员视图通过 admin.site.register(..) 被初始化,这样构造函数就可以开始导入更多的东西。在某个时刻,这可能会触发你模块中的第一个语句。

    我在我的中间件中有这样的语句,你可以猜到我最后遇到了什么问题。 ;)

  2. 混合使用表单字段、小部件和模型。

    因为模型可以提供一个“表单字段”,你开始导入表单。表单有一个小部件。那个小部件又有一些来自……呃……模型的常量。现在你就形成了一个循环。最好把那个表单字段类的导入放在 def formfield() 函数内部,而不是全局模块范围内。

  3. 一个 managers.py 文件引用了 models.py 的常量。

    毕竟,模型需要先有管理器。管理器不能开始导入 models.py,因为它还在初始化中。下面会有更简单的情况说明。

  4. 使用 ugettext() 而不是 ugettext_lazy

    当你使用 ugettext() 时,翻译系统需要初始化。它会扫描 INSTALLED_APPS 中的所有包,寻找 locale.XY.formats 包。当你的应用刚开始初始化时,它又被全局模块扫描导入了。

    类似的事情也会发生在插件扫描、haystack的搜索索引等其他类似机制中。

  5. __init__.py 中放入过多内容。

    这结合了第1点和第4点,它给导入系统带来了压力,因为导入一个子包会首先初始化所有父包。实际上,很多代码会在简单导入时运行,这增加了从错误地方导入东西的可能性。

解决方案也并不难。一旦你了解了造成循环的原因,就可以把那个导入语句从全局导入(文件顶部)移除,放到使用该符号的函数内部。例如:

# models.py:
from django.db import models
from mycms.managers import PageManager

class Page(models.Model)
    PUBLISHED = 1

    objects = PageManager()

    # ....


# managers.py:
from django.db import models

class PageManager(models.Manager):
    def published(self):
        from mycms.models import Page   # Import here to prevent circular imports
        return self.filter(status=Page.PUBLISHED)

在这个例子中,你可以看到 models.py 确实需要导入 managers.py;没有它,就无法完成 PageManager 的静态初始化。反过来就没那么关键了。Page 模型可以很容易地在函数内部导入,而不是全局导入。

同样的情况也适用于其他导入错误。循环可能还会涉及更多的包。

撰写回答