题 用于在Python / Django中精确定位循环导入的工具?


我有一个Django应用程序,其中的某个地方是导致问题的递归导入。由于应用程序的大小,我有一个问题,指出循环导入的原因。

我知道答案是“只是不写循环导入”,但问题是我很难弄清楚循环导入的来源,所以理想情况下,一个跟踪导入回原点的工具将是理想。

这样的工具存在吗?除此之外,我觉得我正在尽我所能避免循环导入问题 - 如果可能的话将导入移到页面底部,将它们移到函数内部而不是将它们放在顶部等等但仍然遇到问题。我想知道是否有任何提示或技巧完全避免它们。

详细说明......

在Django中,当它遇到循环导入时,有时它会抛出一个错误,但有时会无声地传递  导致某些模型或字段的情况 只是不在那里。令人沮丧的是,这通常发生在一个上下文(例如,WSGI服务器)而不是另一个上下文(shell)中。所以在shell中测试这样的东西会起作用:

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

但在网络上抛出错误:

FieldError:无法将关键字“bar__name”解析为字段。选择是:......

几个领域明显缺失。

因此,它不能成为代码的直接问题  在shell工作但是  通过网站。

一些工具可以找出正在发生的事情 ImportError 可能是有用的最不常见的异常消息。


28
2018-02-01 15:46


起源


python -vv将有助于查找递归导入。例: pastebin.com/3HpYgeC2 - jpic
有没有办法以某种方式组织这个输出,所以我可以看到所谓的?似乎这样只适用于硬循环导入问题,而不是像我那样柔软的导入问题...... - Jordan Reiter
(我不确定这会对你有什么帮助,因为我不确定如何在你的WSGI环境中使用它)。无论如何,它可以帮助解决“软”/“运行时”导入问题。像粘贴的第627行我手动调用“import django”:它显示它尝试的所有文件。我刚刚测试了“import django.db”,它显示了它为django.db包含的所有模块尝试的所有文件。我不知道,但想知道现有的方法来改善输出,因为这是一个痛苦,我完全同意! - jpic
静默失败是因为您有多个具有相同名称的模块。然后,python导入顺序(基于pythonpath)是引用。哦,当/如果您更改名称,请确保删除 .pyc 太:):它发生在我身上好几次 - Laur Ivan


答案:


在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的search_index以及其他类似的机制也会发生类似的情况。

  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 model可以很容易地导入函数内而不是全局导入。

这同样适用于任何其他导入错误的情况。然而,该循环可以包括更多的包。


33
2018-02-09 22:16



这个答案会更好,有一个明确的回溯示例,并检测一个真正的循环导入,看看它是如何工作的。我正在盯着一个追溯,我知道这是一个循环导入,但我仍在挖掘5个以上的文件,以确切地确定导入错误的位置。 - Rudolf Olah
每个人都在写'检查你的代码进行循环导入',最后这个答案结果是指向'ugettext'的实际加载行为的救世主。非常感谢你。 - acpmasquerade
ugettext 也帮我了大道具! - yellottyellott
怎么样不进口 Page 经理模特?经理本身有其属性 model 。 - Krystofee


Django中循环导入的常见原因之一是在相互引用的模块中使用外键。 Django通过将模型显式指定为具有完整应用程序标签的字符串,提供了一种绕过这种方法的方法:

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

看到: https://docs.djangoproject.com/en/dev/ref/models/fields/#foreignkey


11
2018-02-09 23:45



如果我能提高10分,我会。这是一个巨大的救生员。 - MagicLAMP
这可能是显而易见的,但请注意,当您使用此方法时,这意味着您不应该在models.py文件的顶部执行导入。因此,首先查看导致ImportError的行,如果是的话 from myapp import MyAppModel,删除该行,而不是如上所述,放 'myapp.MyAppModel' 在外键中。 - Rob


只是在回答中转换上面的评论......

如果你有循环导入, python -vv 诀窍。其他方式是重载模块加载器(某处有链接,但我现在无法找到它)。更新:你可以用它来做 ModuleFinder

发生静默失败是因为您有多个具有相同名称的模块。然后,python导入顺序(基于pythonpath)是引用。哦,当/如果您更改名称,请确保删除 .pyc 太:):它发生在我身上好几次


6
2018-02-09 22:27





当我遇到导入错误时,我通常会做的是向后工作。我会得到一些“无法从myproject.views导入xyz”错误,即使xyz存在就好了。然后我做两件事:

  • 我为myproject.views的每次导入grep我自己的代码,并创建一个导入它的模块的(心理)列表。

  • 我检查是否依次在views.py中导入其中一个匹配的模块。这通常会给你带来罪魁祸首。

一个可能出错的常见地方是你的models.py。通常是你正在做的事情的核心。但请确保您尝试保持导入指向AT models.py而不是远离它。所以从views.py导入模型,但不是相反。

在urls.py中,我通常会导入我的视图(因为当我犯这样的错误时,我得到一个很好的立即导入错误)。但是为了防止循环导入错误,您还可以使用虚线路径字符串引用视图。但这取决于你在urls.py中所做的事情。

关于的评论 进口货物:将它们保存在文件的顶部。如果它们分散了,你将永远不会清楚地了解哪个模块导入了什么。只需将它们全部放在顶部(排序很好)就可以帮助您查明问题。仅在必要时导入内部函数以解决特定的循环导入。

并使您的进口绝对而不是相对。我的意思是“来自myproject.views import xyz”而不是“from views import xyz”。将它与排序导入列表绝对结合使您的导入更加清晰和整洁。


5
2018-02-09 11:02