0

Инструмент для выявленияCircular Imports в Python/Django?

16

У меня есть приложение на Django, и где-то в его коде происходит рекурсивный импорт, который вызывает проблемы. Из-за объема приложения мне сложно определить, в чем именно заключается источник циклического импорта.

Я понимаю, что решение проблемы — это "просто не писать циклические импорты", но на самом деле мне трудно выяснить, откуда именно исходит этот проблемный импорт. Было бы идеально иметь инструмент, который прослеживал бы импорт до его источника.

Существует ли такой инструмент? Откладывая этот вопрос в сторону, я стараюсь избегать проблем с циклическим импортом — перемещаю импорты в конец файла, если это возможно, помещаю их внутрь функций вместо того, чтобы делать их вверху, и так далее, но по-прежнему сталкиваюсь с проблемами. Мне интересно, есть ли какие-то советы или трюки для полного избежания таких ситуаций.

Чтобы подробнее объяснить...

В Django, когда он сталкивается с циклическим импортом, иногда возникает ошибка, но иногда он проходит без сообщений, что приводит к ситуации, когда определенные модели или поля просто отсутствуют. Удивительно, но эта ситуация часто наблюдается в одном контексте (например, WSGI-сервере), но не в другом (в интерактивной оболочке). Поэтому, если протестировать следующее в оболочке, это будет работать:

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

Но на веб-сайте возникает ошибка:

FieldError: Не удается разрешить ключевое слово 'bar__name' в поле. Возможные варианты: ...

Лицом к лицу с несколькими явно отсутствующими полями.

Таким образом, это не может быть просто проблема с кодом, поскольку он работает в оболочке, но не работает через веб-сайт.

Какой-то инструмент, который прояснил бы, в чем дело, был бы отличным. Сообщение исключения ImportError — возможно, одно из наименее полезных.

3 ответ(ов)

0

Причина ошибки импорта легко выявляется в трассировке исключения ImportError.

Когда вы смотрите на трассировку, вы увидите, что модуль был импортирован ранее. Один из его импортов импортировал что-то еще, выполнил основной код, и теперь импортирует тот первый модуль. Поскольку первый модуль не был полностью инициализирован (он все еще завис в коде импорта), вы получаете ошибки о том, что символы не найдены. Это имеет смысл, так как основной код модуля еще не достиг этой точки.

Общие причины в Django:

  1. Импортирование подпакета из совершенно другого модуля:

    Например, from mymodule.admin.utils import ...

    Это приведет к загрузке admin/__init__.py сначала, который, вероятно, импортирует кучу других пакетов (например, модели, представления админки). Представление админки инициализируется с помощью admin.site.register(..), поэтому конструктор может начать импортировать больше модулей. В какой-то момент это может затронуть ваш модуль с первой строкой.

    У меня был такой импорт в промежуточном ПО (middleware), и вы можете догадаться, к чему это привело. 😉

  2. Смешивание полей форм, виджетов и моделей.

    Поскольку модель может предоставлять "formfield", вы начинаете импортировать формы. У неё есть виджет. Этот виджет использует некоторые константы из... ммм... модели. И вот уже у вас есть цикл. Лучше импортировать класс поля формы внутри функции 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   # Импортируем здесь, чтобы предотвратить циклические импорты
        return self.filter(status=Page.PUBLISHED)

В этом случае вы можете увидеть, что models.py действительно нуждается в импорте managers.py; без него он не сможет выполнить статическую инициализацию PageManager. Обратный импорт не так критичен. Модель Page могла быть легко импортирована внутри функции вместо глобально.

То же самое относится к любой другой ситуации с ошибками импорта. Однако цикл может включать несколько других пакетов.

0

Одной из распространённых причин возникновения циклических импортов в Django является использование внешних ключей в модулях, которые ссылаются друг на друга. Django предлагает способ избежать этого, явно указывая модель в виде строки с полным именем приложения:

# from myapp import MyAppModel  ## цикл импорта исключён

class MyModel(models.Model):
    myfk = models.ForeignKey(
        'myapp.MyAppModel',  ## избежание циклического импорта
        null=True)

Смотрите: документация Django по внешним ключам.

0

Когда я сталкиваюсь с ошибкой импорта, я обычно действую в обратном порядке. Например, я получаю ошибку "cannot import xyz from myproject.views", хотя xyz в действительности существует. В таких случаях я делаю две вещи:

  • Использую grep для поиска всех импортов myproject.views в своем коде и составляю (в голове) список модулей, которые его импортируют.
  • Проверяю, импортирую ли я один из совпадающих модулей в views.py. Часто это сразу указывает на источник проблемы.

Одно из самых распространенных мест, где могут возникнуть ошибки - это models.py. Обычно он играет центральную роль в вашей работе. Но постарайтесь, чтобы ваши импорты указывали НА models.py, а не наоборот. Импортируйте модели из views.py, но не наоборот.

В urls.py я обычно импортирую свои представления (это удобно, так как в случае ошибки я сразу получаю сообщение об ошибке). Чтобы избежать ошибок циклического импорта, вы также можете ссылаться на свои представления с помощью строкового пути. Но это зависит от того, что вы делаете в вашем urls.py.

Комментарий по поводу расположения импортов: держите их вверху файла. Если импорты разбросаны по коду, вам будет сложно понять, что что импортирует. Упорядочение импортов в верхней части файла может помочь вам быстрее выявить проблемы. Импортируйте внутри функций лишь в том случае, если это необходимо для решения конкретной проблемы циклического импорта.

И делайте свои импорты абсолютными вместо относительных. То есть пишите "from myproject.views import xyz", а не "from views import xyz". Это сделает ваши импорты более понятными и аккуратными, особенно если вы дополнительно отсортируете список импортов.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь