Как избежать циклического импорта python
Циклический импорт в Python
Циклическая зависимость в Python возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого.
Приведенный выше код демонстрирует довольно очевидную циклическую зависимость. functionA() вызывает functionB(), следовательно, в зависимости от него, а functionB() вызывает functionA(). У этого типа циклической зависимости есть некоторые очевидные проблемы, которые мы опишем немного дальше в следующем разделе.
Проблемы с круговыми зависимостями
Циклические зависимости могут вызвать множество проблем в вашем коде. Например, это может привести к тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также усложняет сопровождение кода в долгосрочной перспективе.
Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и у вас есть циклическая зависимость в вашем коде, может быть очень сложно отладить множество потенциальных проблем, которые он вызывает.
Что такое циклический импорт в Python?
Циклический импорт в Python – это форма циклической зависимости, которая создается с помощью оператора импорта в Python.
Например, давайте проанализируем следующий код:
Когда Python импортирует модуль, он проверяет реестр модулей, чтобы убедиться, что он уже импортирован. Если модуль уже был зарегистрирован, Python использует этот существующий объект из кеша. Реестр модулей – это таблица модулей, которые были инициализированы и проиндексированы по имени модуля. К этой таблице можно получить доступ через sys.modules.
Если он не был зарегистрирован, Python находит модуль, при необходимости инициализирует его и выполняет в пространстве имен нового модуля.
В нашем примере, когда Python достигает import module2, он загружает и выполняет его. Однако module2 также вызывает module1, который, в свою очередь, определяет function1().
Проблема возникает, когда function2() пытается вызвать функцию module1 function3(). Поскольку модуль1 был загружен первым и, в свою очередь, загружен модуль2 до того, как он смог достичь function3(), эта функция еще не определена и выдает ошибку при вызове:
Как исправить?
Как правило, циклический импорт – это результат плохого дизайна. Более глубокий анализ программы мог бы сделать вывод, что зависимость на самом деле не требуется или что зависимые функции могут быть перемещены в другие модули, которые не будут содержать циклическую ссылку.
Простое решение состоит в том, что иногда оба модуля можно просто объединить в один более крупный. Результирующий код из нашего примера выше будет выглядеть примерно так:
Однако объединенный модуль может иметь некоторые несвязанные функции (тесная связь) и может стать очень большим, если в двух модулях уже есть много кода.
Так что, если это не сработает, можно было бы отложить импорт module2, чтобы импортировать его только тогда, когда это необходимо. Это можно сделать, поместив импорт module2 в определение function1():
В этом случае Python сможет загрузить все функции в module1, а затем загрузить module2 только при необходимости.
Этот подход не противоречит синтаксису, поскольку в документации сказано: «Обычно, но не обязательно, помещать все операторы импорта в начало модуля».
В документации также говорится, что рекомендуется использовать import X вместо других операторов, таких как from module import * или from module import a, b, c.
Заключение
Циклический импорт – это особый случай циклических ссылок. Как правило, их можно решить с помощью улучшенного дизайна кода. Однако иногда результирующий дизайн может содержать большой объем кода или смешивать несвязанные функции (жесткая связь).
Круговой (или циклический) импорт в Python
Что произойдет, если два модуля импортируют друг друга?
Чтобы обобщить проблему, как насчет циклического импорта в Python?
11 ответов
Это было действительно хорошее обсуждение на comp.lang.python в прошлом году Он довольно тщательно отвечает на ваш вопрос.
Импорт довольно прост на самом деле. Просто запомните следующее:
‘import’ и ‘from xxx import yyy’ являются исполняемыми операторами. Они выполняются, когда запущенная программа достигает этой строки.
Если модуль отсутствует в sys.modules, то при импорте создается новая запись модуля в sys.modules, а затем выполняется код в модуле. Он не возвращает управление вызывающему модулю, пока выполнение не завершится.
Если модуль существует в sys.modules, то импорт просто возвращает этот модуль независимо от того, завершился он или нет. По этой причине циклический импорт может возвращать модули, которые кажутся частично пустыми.
Наконец, исполняемый скрипт выполняется в модуле с именем __main__, импорт скрипта под собственным именем создаст новый модуль, не связанный с __main__.
Возьмите это вместе, и вы не должны удивляться при импорте модулей.
Файл б:
Здесь много хороших ответов. Хотя обычно есть быстрые решения проблемы, некоторые из которых кажутся более питонными, чем другие, если у вас есть роскошь провести некоторый рефакторинг, другой подход заключается в том, чтобы проанализировать организацию вашего кода и попытаться удалить циклическую зависимость. Например, вы можете обнаружить, что у вас есть:
Файл a.py
Файл b.py
В этом случае просто переместите один статический метод в отдельный файл, скажем c.py :
Файл c.py
Позволит удалить метод save_result из A и, следовательно, разрешить удаление импорта A из a в b:
Восстановленный файл a.py
Восстановленный файл b.py
Таким образом, если у вас есть инструмент (например, pylint или PyCharm), который сообщает о методах, которые могут быть статическими, простое добавление к ним декоратора staticmethod может быть не лучшим способом заставить замолчать предупреждение. Несмотря на то, что метод, похоже, связан с классом, может быть, лучше выделить его, особенно если у вас есть несколько тесно связанных модулей, которым может потребоваться такая же функциональность, и вы намерены применять принципы СУХОЙ.
Я полностью согласен с ответом питона здесь. Но я наткнулся на некоторый код, который имел недостатки при циклическом импорте и вызывал проблемы при попытке добавить модульные тесты. Таким образом, чтобы быстро исправить это без изменения всего, вы можете решить проблему, выполнив динамический импорт.
Опять же, это не постоянное исправление, но может помочь кому-то, кто хочет исправить ошибку импорта, не изменяя слишком много кода.
Круговой импорт может сбивать с толку, потому что импорт делает две вещи:
Круговой импорт не является последним злом, которого следует избегать любой ценой. В некоторых средах, таких как Flask, они вполне естественны, и настройка вашего кода для их устранения не делает код лучше.
Web-приложения на Flask: как бороться с циклическими импортами
Flask – один из самых популярных фреймворков Python, но некоторые ошибки при его использовании могут привести к определенным затруднениям. В этой статье мы расскажем о том, как не допустить возникновения циклических импортов в проекте.
Flask и циклические импорты
Разработчики, использующие flask, нередко сталкиваются с проблемой возникновения зависимостей между модулями. Для объявления view и моделей разработчик использует глобальные объекты, созданные и инициализированные в главном модуле («точке входа»). При этом он рискует получить циклические импорты, из-за которых проект будет трудно поддерживать.
Документация и основные учебники flask для решения этой проблемы предлагают вынести в __init__.py код инициализации проекта, который создает инстанс-классы Flask и производит настройку приложения. Это позволяет получить доступ ко всем глобальным объектам из области видимости пакета.
При использовании этого подхода структура выглядит примерно так:
Очевидно, что эта архитектура не очень удачна, поскольку все компоненты сильно связаны. Впоследствии дорабатывать такой проект будет сложно, потому что изменение кода в одном месте потянет за собой изменения в десятке других мест.
Как правило, мы решаем эту проблему следующим образом:
Работа с classy
Вместо стандартного способа роутинга, который описан в документации, можно применять classy. С этим подходом вам не нужно вручную писать роутинг для view: он настроится автоматически на основании имен ваших классов и методов. Это позволяет повышать структурированность кода, а также объявлять view без объекта app. В результате удается решить проблему циклического импорта.
Пример структуры проекта при использовании библиотеки flask-classful.
При изучении кода необходимо обратить внимание на то, что теперь инициализация происходит в app.py, расположенном в корне. Приложение разбито на подпроекты, которые конфигурируются при помощи blueprint и в дальнейшем одной строкой регистрируются в объекте app.
Предпочтение оригинальным версиям библиотек
Приведенный выше код показывает, как flask-classful помогает бороться с циклическими импортами. Причиной этой проблемы в классических проектах flask могут быть как объявление view, так и некоторые расширения. Один из ярких примеров – flask-sqlalchemy.
Расширение flask-sqlalchemy призвано улучшать интеграцию sqlalchemy и flask, но на практике оно зачастую привносит в проект больше проблем, чем пользы:
Использование паттерна Dependency injection
Внедрение classy-подхода и отказ от flask-sqlalchemy – это лишь первые шаги для решения проблемы циклического импорта. Далее нужно реализовать в приложении логику доступа к глобальным объектам. Для этого удобно применять паттерн dependency injection, реализованный в библиотеке dependency-injector.
Пример использования паттерна в коде с библиотекой dependency-injector:
Перечисленные в статье меры позволяют устранить циклические импорты, а также повысить качество кода. Предлагаем посмотреть, как выглядит flask-проект с использованием описанных выше подходов, на примере игры «Быки и коровы», выполненной в виде web-приложения.
Вывод
Мы рассмотрели, какими способами можно решить распространенную архитектурную проблему flask-приложений, связанную с циклическими импортами. С их помощью вы можете упростить доработку и поддержку ваших приложений.
Спасибо за внимание! Надеемся, что эта статья была вам полезна.
Как избежать циклического импорта в Python?
Я знаю, что проблема циклического импорта в python возникла много раз раньше, и я прочитал эти обсуждения. Комментарий, который неоднократно повторяется в этих обсуждениях, заключается в том, что циклический импорт является признаком плохой конструкции, и код должен быть реорганизован, чтобы избежать циклического импорта.
Может ли кто-нибудь сказать мне, как избежать циклического импорта в этой ситуации?: У меня есть два класса, и я хочу, чтобы каждый класс имел конструктор (метод), который принимает экземпляр другого класса и возвращает экземпляр класса.
Более конкретно, один класс является изменяемым, а один неизменным. Требуется неизменный класс для хэширования, сравнения и т.д. Переменный класс необходим, чтобы что-то делать. Это похоже на наборы и фризонсет, или на списки и кортежи.
Я мог бы поместить оба определения классов в один и тот же модуль. Есть ли другие предложения?
Примером игрушки может быть класс A, который имеет атрибут, который является списком и классом B, который имеет атрибут, который является кортежем. Затем класс A имеет метод, который принимает экземпляр класса B и возвращает экземпляр класса A (путем преобразования кортежа в список), и аналогично класс B имеет метод, который принимает экземпляр класса A и возвращает экземпляр класса B (путем преобразования списка в кортеж).
ОТВЕТЫ
Ответ 1
Только импортируйте модуль, не импортируйте его из модуля:
Это прекрасно работает.
Ответ 2
Рассмотрим следующий пример пакета Python, в котором a.py и b.py зависят друг от друга:
Типы проблем кругового импорта
Круговые зависимости импорта обычно делятся на две категории в зависимости о том, что вы пытаетесь импортировать и где вы используете его внутри каждого модуль. (И используете ли вы Python 2 или 3).
1. Ошибки при импорте модулей с циклическим импортом
В некоторых случаях просто импортируйте модуль с циклической зависимостью импорта может привести к ошибкам, даже если вы ничего не ссылаетесь на импортированный модуль.
Есть несколько стандартных способов импортировать модуль в Python
К сожалению, только 1-й и 4-й варианты действительно работают, когда вы имеют круговые зависимости (остальные все поднимают ImportError или AttributeError ). В общем, вы не должны использовать 4-й синтаксис, так как он работает только в Python2 и рискует конфликт с другими сторонними модулями. Так реально только первый синтаксис гарантированно работает.
ОБНОВЛЕНИЕ: проблемы ImportError и AttributeError возникают только в Python 2. В Python 3 была переписана импортная техника и все из этих операторов импорта (за исключением 4) будет работать, даже с круговые зависимости. Решения в этом разделе предназначены для людей, использующих Python 2.
Абсолютный импорт
Просто используйте первый синтаксис импорта выше. Недостатком этого метода является что имена импорта могут быть очень длинными для больших пакетов.
Отложить импорт на потом
Я видел этот метод, используемый во многих пакетах, но он все еще чувствует мне повезло, и мне не нравится, что я не могу посмотреть на верхнюю часть модуля и увидеть все его зависимости, я должен искать все функции также.
Поместите весь импорт в центральный модуль
2. Ошибки при использовании импортированных объектов с круговыми зависимостями
Теперь, хотя вы можете импортировать модуль с циклическим импортом зависимость, вы не сможете импортировать объекты, определенные в модуле или на самом деле сможет ссылаться на этот импортированный модуль в любом месте в верхнем уровне модуля, куда вы его импортируете. Ты можешь, однако используйте импортированный модуль внутри функций и блоков кода, которые не запустить при импорте.
Например, это будет работать:
Но это не сработает
Вы получите исключение
AttributeError: модуль ‘package’ не имеет атрибута ‘a’
Как правило, в большинстве действительных случаев циклических зависимостей возможно реорганизовать или реорганизовать код для предотвращения этих ошибок и перемещения ссылки на модули внутри блока кода.
Ответ 3
Мы выполняем комбинацию абсолютного импорта и функций для лучшего чтения и более коротких строк доступа.
Циклический импорт Python
Что такое круговая зависимость?
Циклическая зависимость возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого (см. Рисунок 1).
Проблемы с круговыми зависимостями
Циклические зависимости могут вызвать множество проблем в вашем коде. Например, это может привести к тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также усложняет сопровождение кода в долгосрочной перспективе.
Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и в вашем коде будет циклическая зависимость, может быть очень сложно отладить множество потенциальных проблем, которые он вызывает.
Что такое циклический импорт?
Например, давайте проанализируем следующий код:
Если он не был зарегистрирован, Python находит модуль, при необходимости инициализирует его и выполняет в пространстве имен нового модуля.
Как исправить круговые зависимости
Простое решение состоит в том, что иногда оба модуля можно просто объединить в один более крупный модуль. Результирующий код из нашего примера выше будет выглядеть примерно так:
Однако объединенный модуль может иметь некоторые несвязанные функции (тесная связь) и может стать очень большим, если в двух модулях уже есть много кода.
Так что, если это не сработает, другим решением может быть отложить импорт module2, чтобы импортировать его только тогда, когда это необходимо. Это можно сделать, поместив импорт module2 в определение function1() :
В этом случае Python сможет загрузить все функции в module1, а затем загрузить module2 только при необходимости.
Этот подход не противоречит синтаксису Python, поскольку документация Python гласит : «Обычно, но не обязательно размещать все операторы импорта в начале модуля (или сценария, если на то пошло)».
Подведение итогов
Вы сталкивались с циклическим импортом в собственном коде? Если да, то как вы это исправили? Дайте нам знать об этом в комментариях!