Rukovodstvo
статьи и идеи для разработчиков программного обеспечения и веб-разработчиков.
Циклический импорт Python
Что такое круговая зависимость? Циклическая зависимость возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого (см. Рисунок 1). Например: functionA (): functionB () And functionB (): functionA () Приведенный выше код демонстрирует довольно очевидную циклическую зависимость. functionA () вызывает functionB (), следовательно, в зависимости от него, а functionB () вызывает functionA (). У этого типа циклической зависимости есть некоторые очевидные проблемы, которые мы
Время чтения: 4 мин.
Что такое круговая зависимость?
Циклическая зависимость возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого (см. Рисунок 1).
Приведенный выше код демонстрирует довольно очевидную циклическую зависимость. functionA() вызывает functionB() , следовательно, в зависимости от него, а functionB() вызывает functionA() . У этого типа циклической зависимости есть некоторые очевидные проблемы, которые мы опишем немного дальше в следующем разделе.
Проблемы с круговыми зависимостями
Циклические зависимости могут вызвать множество проблем в вашем коде. Например, это может привести к тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также усложняет сопровождение кода в долгосрочной перспективе.
Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и в вашем коде будет циклическая зависимость, может быть очень сложно отладить множество потенциальных проблем, которые он вызывает.
Что такое циклический импорт?
Циклический импорт — это форма циклической зависимости, которая создается с помощью оператора импорта в Python.
Например, давайте проанализируем следующий код:
Когда Python импортирует модуль, он проверяет реестр модулей, чтобы убедиться, что модуль уже импортирован. Если модуль уже был зарегистрирован, Python использует этот существующий объект из кеша. Реестр модулей — это таблица модулей, которые были инициализированы и проиндексированы по имени модуля. Доступ к этой таблице можно получить через sys.modules .
Если он не был зарегистрирован, Python находит модуль, при необходимости инициализирует его и выполняет в пространстве имен нового модуля.
В нашем примере, когда Python достигает import module2 , он загружает и выполняет его. Однако module2 также вызывает module1, который, в свою очередь, определяет function1() .
Проблема возникает, когда function2() пытается вызвать function3() . Поскольку модуль1 был загружен первым и, в свою очередь, загружен модуль2 до того, как он смог достичь function3() , эта функция еще не определена и выдает ошибку при вызове:
Как исправить круговые зависимости
Как правило, циклический импорт — это результат плохого дизайна. Более глубокий анализ программы мог бы сделать вывод, что зависимость на самом деле не требуется или что зависимые функции могут быть перемещены в другие модули, которые не будут содержать циклическую ссылку.
Простое решение состоит в том, что иногда оба модуля можно просто объединить в один более крупный модуль. Результирующий код из нашего примера выше будет выглядеть примерно так:
Однако объединенный модуль может иметь некоторые несвязанные функции (тесная связь) и может стать очень большим, если в двух модулях уже есть много кода.
Так что, если это не сработает, другим решением может быть отложить импорт module2, чтобы импортировать его только тогда, когда это необходимо. Это можно сделать, поместив импорт module2 в определение function1() :
В этом случае Python сможет загрузить все функции в module1, а затем загрузить module2 только при необходимости.
Этот подход не противоречит синтаксису Python, поскольку документация Python гласит : «Обычно, но не обязательно размещать все операторы импорта в начале модуля (или сценария, если на то пошло)».
В документации Python также говорится, что рекомендуется использовать import X вместо других операторов, таких как from module import * или from module import a,b,c .
Вы также можете увидеть много кодовых баз, использующих отложенный импорт, даже если нет циклической зависимости, которая ускоряет время запуска, поэтому это вообще не считается плохой практикой (хотя это может быть плохой дизайн, в зависимости от вашего проекта) .
Подведение итогов
Циклический импорт — это особый случай циклических ссылок. Как правило, их можно решить с помощью лучшего дизайна кода. Однако иногда результирующий дизайн может содержать большой объем кода или смешивать несвязанные функции (тесная связь).
Вы сталкивались с циклическим импортом в собственном коде? Если да, то как вы это исправили? Дайте нам знать об этом в комментариях!
Современные среды Python — управление зависимостями и рабочим пространством
После того, как вы пройдете через муки настройки среды Python для одного приложения типа «hello world», вам придется пройти через еще более сложный процесс выяснения, как управлять несколькими средами для нескольких проектов Python. Некоторые из этих проектов могут быть новыми, в то время как другие представляют собой груды кода десятилетней давности. К счастью, существует ряд инструментов, помогающих упростить управление зависимостями и рабочими пространствами.
В этой статье мы рассмотрим доступные инструменты для управления зависимостями и рабочим пространством, чтобы решить следующие проблемы:
- Установка и переключение между разными версиями Python на одном компьютере
- Управление зависимостями и виртуальными средами
- Воспроизведение сред
Установка Python
Хотя вы можете загрузить и установить Python из официальных двоичных файлов или с помощью менеджера пакетов вашей системы, вам следует воздержаться от этих подходов, если только вам не посчастливилось использовать одну и ту же версию Python для всех ваших текущих и будущих проектов. Поскольку это, скорее всего, не так, мы рекомендуем устанавливать Python с помощью pyenv.
pyenv — это инструмент, который упрощает установку и переключение между различными версиями Python на одной машине. Он сохраняет системную версию Python, которая необходима для нормальной работы некоторых операционных систем, и в то же время позволяет легко переключать версии Python в зависимости от требований конкретного проекта.
К сожалению, pyenv не работает под Windows вне Windows Subsystem for Linux. Проверьте pyenv-win, если это ваш случай.
После установки вы можете легко установить определенную версию Python следующим образом:
Затем вы можете установить глобальную версию Python следующим образом:
Помните, что это не изменяет и не влияет на Python на системном уровне.
Аналогичным образом вы можете установить интерпретатор Python для текущей папки:
Теперь каждый раз, когда вы запускаете Python внутри этой папки, будет использоваться версия 3.9.0.
Управление зависимостями
В этом разделе мы рассмотрим несколько инструментов для управления зависимостями, а также виртуальными средами.
venv + pip
venv и pip (package installer for python), которые поставляются с большинством версий Python, являются наиболее популярными инструментами для управления виртуальными средами и пакетами, соответственно. Они довольно просты в использовании.
Виртуальные среды предотвращают конфликты версий зависимостей. Вы можете установить разные версии одной и той же зависимости в разных виртуальных средах.
Вы можете создать новую виртуальную среду под названием my_venv внутри текущей папки следующим образом:
После создания среды ее необходимо активировать, выполнив скрипт activate внутри виртуальной среды:
Для деактивации выполните команду deactivate . Затем, для повторной активации, выполните source my_venv/bin/activate в корневом каталоге проекта.
Запуск which python при активированной виртуальной среде вернет путь к интерпретатору Python внутри виртуальной среды:
Вы можете установить пакеты, локальные для вашего проекта, запустив pip install с активированной виртуальной средой:
pip загружает пакет из PyPI (Python Package Index) и затем делает его доступным для интерпретатора Python внутри виртуальной среды.
Для воспроизводимости среды обычно требуется хранить список необходимых пакетов для проекта в файле requirements.txt. Вы можете вручную создать этот файл и добавить их или использовать команду pip freeze для его генерации:
Хотите взять только зависимости верхнего уровня (например, requests==2.24.0 )? Посмотрите pip-chill.
Хотя venv и pip просты в использовании, они очень примитивны по сравнению с более современными инструментами, такими как Poetry и Pipenv. venv и pip ничего не знают о версии Python, с которой они работают. Вам придется управлять всеми зависимостями и виртуальными окружениями вручную. Вам придется самостоятельно создавать и управлять файлом requirements.txt. Более того, вам придется вручную разделять зависимости разработки (pytest, black, isort, . ) и производства (Flask, Django, FastAPI, . ), используя файл requirements-dev.txt.
Poetry и Pipenv объединяют функциональность venv и pip. Они также позволяют легко разделять зависимости для разработки и производства, а также обеспечивают детерминированные сборки с помощью файла блокировки. Они хорошо работают с pyenv.
Файлы блокировки фиксируют (или блокируют) все версии зависимостей по всему дереву зависимостей.
Poetry
Poetry — это, пожалуй, самый функциональный инструмент управления зависимостями для Python. Он поставляется с мощным CLI, используемым для создания и управления проектами Python. После установки для создания нового проекта выполните:
В результате будут созданы следующие файлы и папки:
Управление зависимостями осуществляется внутри файла pyproject.toml:
Подробнее о pyproject.toml, новом файле конфигурации пакетов Python, который рассматривает «каждый проект как пакет», читайте в заметке Что это за хрень — pyproject.toml?.
Чтобы добавить новую зависимость, просто выполните:
Флаг —dev указывает, что зависимость предназначена только для использования в режиме разработки. Зависимости для разработки не устанавливаются по умолчанию.
Это загружает и устанавливает Flask из PyPI в виртуальную среду, управляемую Poetry, добавляет ее вместе со всеми подзависимостями в файл poetry.lock и автоматически добавляет ее (зависимость верхнего уровня) в pyproject.toml:
Обратите внимание на ограничение версии : «^1.1.2» .
Чтобы выполнить команду внутри виртуальной среды, добавьте к ней префикс poetry run. Например, для запуска тестов с помощью команды pytest :
poetry run будет выполнять команды внутри виртуальной среды. Однако это не активирует виртуальную среду. Чтобы активировать виртуальную среду Poetry, нужно выполнить команду poetry shell . Чтобы деактивировать ее, достаточно выполнить команду exit . Следовательно, вы можете активировать виртуальную среду перед началом работы над проектом и деактивировать ее по окончании, или вы можете использовать poetry run на протяжении всей разработки.
Наконец, Poetry хорошо работает с pyenv. Просмотрите Managing environments из официальной документации, чтобы узнать больше об этом.
Pipenv
Pipenv пытается решить те же проблемы, что и Poetry:
- Managing dependencies and virtual environments
- Reproducing environments
После установки, чтобы создать новый проект с помощью Pipenv, выполните:
Это создаст новую виртуальную среду и добавит Pipfile к проекту:
A Pipfile работает так же, как файл pyproject.toml в стране Поэзии.
Вы можете установить новую зависимость следующим образом:
Флаг —dev указывает, что зависимость предназначена только для использования в режиме разработки. Зависимости для разработки не устанавливаются по умолчанию.
Как и в случае с Poetry, Pipenv загружает и устанавливает Flask внутри виртуальной среды, фиксирует все подзависимости в файле Pipfile.lock и добавляет зависимость верхнего уровня в Pipfile.
Чтобы запустить сценарий внутри виртуальной среды, управляемой Pipenv, необходимо запустить его с помощью команды pipenv run. Например, чтобы запустить тесты с помощью команды pytest , выполните:
Как и Poetry, pipenv run будет выполнять команды изнутри виртуальной среды. Чтобы активировать виртуальную среду Pipenv, нужно выполнить pipenv shell . Чтобы деактивировать его, можно выполнить exit .
Pipenv также хорошо работает с pyenv. Например, когда вы хотите создать виртуальную среду из версии Python, которая у вас не установлена, он спросит, хотите ли вы сначала установить ее с помощью pyenv:
Рекомендации
Какой мне использовать?
Рекомендуется начать с venv и pip. С ними проще всего работать. Ознакомьтесь с ними и выясните самостоятельно, в чем они хороши, а где их не хватает.
Поэзия или пипенв?
Поскольку они оба решают одни и те же проблемы, все сводится к личным предпочтениям.
- Публикация в PyPI намного проще с Poetry, поэтому, если вы создаете пакет Python, используйте Poetry.
- Оба проекта довольно медленны, когда дело доходит до разрешения зависимостей, поэтому, если вы используете Docker, вы можете держаться подальше от них обоих.
- С точки зрения разработки с открытым исходным кодом Poetry работает быстрее и, возможно, лучше реагирует на отзывы пользователей.
Дополнительные инструменты
В дополнение к вышеперечисленным инструментам посмотрите на следующие, которые помогут установить и переключиться между различными версиями Python на одной машине, управлять зависимостями и виртуальными средами, а также воспроизводить среды:
- Docker — это платформа для создания, развертывания и управления контейнерными приложениями. Он идеально подходит для создания воспроизводимых сред. Conda, довольно популярная в сообществе специалистов по данным и машинному обучению, может помочь в управлении зависимостями и виртуальными средами, а также в воспроизведении сред. Когда вам нужно просто упростить переключение между виртуальными средами и управлять ими в одном месте, стоит обратить внимание на virtualenvwrapper и pyenv-virtualenv , плагин pyenv. pip-tools упрощает управление зависимостями и воспроизводимость среды. Это часто сочетается с venv.
Управление проектом
Давайте рассмотрим, как управлять проектом Flask с помощью pyenv и Poetry.
Сначала создайте новый каталог под названием «flask_example» и переместитесь в него:
Во-вторых, установите версию Python для проекта с помощью pyenv:
Далее, инициализируйте новый проект Python с помощью Poetry:
Последнее, но не менее важное, добавить pytest в качестве зависимости для разработки:
Добавьте файл с именем test_app.py:
После этого добавьте базовое приложение Flask в новый файл с именем app.py:
Теперь, чтобы запустить тесты, выполните:
И вы можете запустить сервер разработки следующим образом:
Команда poetry run запускает команду внутри виртуальной среды Poetry.
Заключение
В этой статье были рассмотрены наиболее популярные инструменты для решения следующих проблем, связанных с управлением зависимостями и рабочим пространством:
- Installing and switching between different versions of Python on the same machine
- Managing dependencies and virtual environments
- Reproducing environments
Меньшее значение имеет то, какие именно инструменты вы используете в своем рабочем процессе, и большее — то, что вы способны решить эти проблемы. Выбирайте те инструменты, которые облегчают вам разработку на Python. Экспериментируйте. Они существуют для того, чтобы облегчить ваш ежедневный рабочий процесс разработки, чтобы вы могли стать настолько продуктивными, насколько это возможно. Попробуйте все из них и используйте те, которые подходят для вашего стиля разработки. Никаких суждений.
Управление зависимостями в Python: сравнение подходов

Я пишу на питоне лет пять, из них последние три года — развиваю собственный проект. Большую часть этого пути мне помогает в этом моя команда. И с каждым релизом, с каждой новой фичей у нас все больше усилий уходит на то, чтобы проект не превращался в месиво из неподдерживаемого кода; мы боремся с циклическими импортами, взаимными зависимостями, выделяем переиспользуемые модули, перестраиваем структуру.
К сожалению, в Python-сообществе нет универсального понятия «хорошей архитектуры», есть только понятие «питоничности», поэтому архитектуру приходится придумывать самим. Под катом — лонгрид с размышлениями об архитектуре и в первую очередь — об управлении зависимостями применимо к Python.
django.setup()
Начну с вопроса джангистам. Часто ли вы пишете вот эти две строчки?
С этого нужно начать файл, если вы хотите поработать с объектами django, не запуская сам вебсервер django. Это касается и моделей, и инструментов работы со временем ( django.utils.timezone ), и урлов ( django.urls.reverse ), и многого другого. Если этого не сделать, то вы получите ошибку:
Я постоянно пишу эти две строчки. Я большой любитель кода «на выброс»; мне нравится создать отдельный .py -файл, покрутить в нем какие-то вещи, разобраться в них — а потом встроить в проект.
И меня очень раздражает этот постоянный django.setup() . Во-первых, устаешь это везде повторять; а, во-вторых, инициализация django занимает несколько секунд (у нас большой монолит), и, когда перезапускаешь один и тот же файл 10, 20, 100 раз — это просто замедляет разработку.
Как избавиться от django.setup() ? Нужно писать код, который по минимуму зависит от django.
Например, если мы пишем некий клиент внешнего API, то можно сделать его зависимым от django:
а можно — независимым от django:
Во втором случае конструктор более громоздкий, зато любые манипуляции с этим классом можно делать, не загружая всю джанговскую машинерию.
Тесты тоже становятся проще. Как тестировать компонент, который зависит от настроек django.conf.settings ? Только замокав их декоратором @override_settings . А если компонент ни от чего не зависит, то и мокать будет нечего: передал параметры в конструктор — и погнали.
Управление зависимостями
История с зависимостью от django — это наиболее яркий пример проблемы, с которой я сталкиваюсь каждый день: проблемы управления зависимостями в python — и в целом выстраивания архитектуры python-приложений.
Отношение к управлению зависимостями в Python-сообществе неоднозначное. Можно выделить три основных лагеря:
-
Питон — гибкий язык. Пишем как хотим, зависим от чего хотим. Не стесняемся циклических зависимостей, подмены атрибутов у классов в рантайме и т.д.
Brandon Rhodes, Dropbox: Hoist your IO.
Пример из доклада:
Недавно я прочел Clean Architecture — и, кажется, понял, в чем ценность внедрения зависимостей в питоне и как его можно реализовать. Я увидел это на примере своего собственного проекта. Вкратце — это защита кода от поломок при изменениях другого кода.
Исходные данные
Есть API-клиент, который выполняет HTTP-запросы на сервис-укорачиватель:
И есть модуль, который укорачивает все ссылки в тексте. Для этого он использует API-клиент укорачивателя:
Логика выполнения кода живет в отдельном управляющем файле (назовем его контроллером):
Всё работает. Процессор парсит текст, укорачивает ссылки с помощью укорачивателя, возвращает результат. Зависимости выглядят вот так:

Проблема
Проблема вот какая: класс TextProcessor зависит от класса ShortenerClient — и сломается при изменении интерфейса ShortenerClient .
Как это может произойти?
Допустим, в нашем проекте мы решили отслеживать переходы по ссылкам и добавили в метод shorten_link аргумент callback_url . Этот аргумент означает адрес, на который должны приходить уведомления при переходе по той или иной ссылке.
Метод ShortenerClient.shorten_link стал выглядеть вот так:
И что получается? А получается то, что при попытке запуска мы получим ошибку:
То есть мы изменили укорачиватель, но сломался не он, а его клиент:

Ну и что такого? Ну сломался вызывающий файл, мы пошли и поправили его. В чем проблема-то?
Если это решается за минуту — пошли и поправили — то это, конечно, и не проблема вовсе. Если в классах мало кода и если вы поддерживаете их самостоятельно (это ваш сайд-проект, это два небольших класса одной подсистемы и тд) — то на этом можно остановиться.
Проблемы начинаются, когда:
- в вызывающем и вызываемом модулях много кода;
- поддержкой разных модулей занимаются разные люди/команды.
Еще интереснее — когда ваш модуль используется в нескольких местах, а не в одном; и ваша правка поломает код в куче файлов.
Поэтому задачу можно сформулировать так: как организовать код так, чтобы при изменении интерфейса ShortenerClient ломался сам ShortenerClient , а не его потребители (которых может быть много)?
Решение здесь такое:
- Потребители класса и сам класс должны договориться об общем интерфейсе. Этот интерфейс должен стать законом.
- Если класс перестанет соответствовать своему интерфейсу — это будут уже его проблемы, а не проблемы потребителей.
Замораживаем интерфейс
Как в питоне выглядит фиксация интерфейса? Это абстрактный класс:
Если теперь мы унаследуемся от этого класса и забудем реализовать какой-то метод — мы получим ошибку:
Но этого недостаточно. Абстрактный класс фиксирует только названия методов, но не их сигнатуру.
Нужен второй инструмент для проверки сигнатуры Этот второй инструмент — mypy . Он поможет проверить сигнатуры унаследованных методов. Для этого мы должны добавить в интерфейс аннотации:
Если теперь проверить этот код при помощи mypy , мы получим ошибку из-за лишнего аргумента callback_url :
Теперь у нас есть надежный способ зафиксировать интерфейс класса.
Инверсия зависимости
Отладив интерфейс, мы должны переместить его в другое место, чтобы окончательно устранить зависимость потребителя от файла shortener_client.py . Например, можно перетащить интерфейс прямо в потребителя — в файл с процессором TextProcessor :
И это изменит направление зависимости! Теперь интерфейсом взаимодействия владеет TextProcessor , и в результате ShortenerClient зависит от него, а не наоборот.

В простых словах можно описать суть нашего преобразования так:
- TextProcessor говорит: я процессор, и я занимаюсь преобразованием текста. Я не хочу ничего знать о механизме укорачивания: это не моё дело. Я хочу дернуть метод shorten_link , чтоб он мне всё укоротил. Поэтому будьте добры, передайте мне объект, который играет по моим правилам. Решения о способе взаимодействия принимаю я, а не он.
- ShortenerClient говорит: похоже, я не могу существовать в вакууме, и от меня требуют определенного поведения. Пойду спрошу у TextProcessor , чему мне нужно соответствовать, чтобы не ломаться.
Несколько потребителей
Если же укорачиванием ссылок пользуются несколько модулей, то интерфейс нужно положить не в одного из них, а в какой-то отдельный файл, который находится «над» остальными файлами, выше по иерархии:

Управляющий компонент
Если потребители не импортируют ShortenerClient , то кто все-таки его импортирует и создает объект класса? Это должен быть управляющий компонент — в нашем случае это controller.py .
Самый простой подход — это прямолинейное внедрение зависимостей, Dependency Injection «в лоб». Создаём объекты в вызывающем коде, передаем один объект в другой. Профит.
Питоничный подход
Считается, что более «питоничный» подход — это Dependency Injection через наследование.
Чтобы адаптировать код под этот стиль, нужно немного поменять TextProcessor , сделав его наследуемым:
И затем, в вызывающем коде, унаследовать его:
Второй пример повсеместно встречается в популярных фреймворках:
- В Django мы постоянно наследуемся. Мы переопределяем методы Class-based вьюх, моделей, форм; иначе говоря, инджектим свои зависимости в уже отлаженную работу фреймворка.
- В DRF — то же самое. Мы расширяем вьюсеты, сериализаторы, пермишены.
- И так далее. Примеров масса.
Развитие питоничного подхода
В бизнес-логике обычно больше двух компонентов. Предположим, что наш TextProcessor , — это не самостоятельный класс, а лишь один из элементов пайплайна TextPipeline , который обрабатывает текст и шлет его на почту:
Если мы хотим изолировать TextPipeline от используемых классов, мы должны проделать такую же процедуру, что и раньше:
- класс TextPipeline будет декларировать интерфейсы для используемых компонентов;
- используемые компоненты будут вынуждены соответствовать этим интерфейсам;
- некий внешний код будет собирать все воедино и запускать.

Но как теперь будет выглядеть код сборки этих зависимостей?
Заметили? Мы сначала наследуем класс TextProcessor , чтобы вставить в него ShortenerClient , а потом наследуем TextPipeline , чтобы вставить в него наш переопределенный TextProcessor (а также Mailer ). У нас появляется несколько уровней последовательного переопределения. Уже сложновато.
Почему же все фреймворки организованы именно таким образом? Да потому, что это подходит только для фреймворков.
- Все уровни фреймворка четко определены, и их количество ограничено. Например, в Django можно переопределить FormField , чтобы вставить его в переопределение формы Form , чтобы вставить форму в переопределение View . Всё. Три уровня.
- Каждый фреймворк служит одной задаче. Эта задача четко определена.
- У каждого фреймворка есть подробная документация, в которой описано, как и что наследовать; что и с чем комбинировать.
Вернемся к подходу «в лоб»
На нескольких уровнях сложности выигрывает простой подход. Он выглядит проще — и его легче менять, когда меняется логика.
Но, когда количество уровней логики возрастает, даже такой подход становится неудобным. Нам приходится в императивном ключе инициировать кучу классов, передавая их друг в друга. Хочется избежать множества уровней вложенности.
Попробуем еще один заход.
Глобальное хранилище инстансов
Попробуем создать некий глобальный словарь, в котором будут лежать инстансы нужных нам компонентов. И пусть эти компоненты достают друг друга через обращение к этому словарю.
Назовем его INSTANCE_DICT :
Трюк — в том, чтобы подложить в этот словарь наши объекты до того, как к ним обратятся. Это мы и сделаем в controller.py :
Плюсы работы через глобальный словарь:
- никакой подкапотной магии и лишних DI-фреймворков;
- плоский список зависимостей, в котором не нужно управлять вложенностью;
- все бонусы DI: простое тестирование, независимость, защита компонентов от поломок при изменениях других компонентов.
Возможно, в какой-то момент мне станет этого мало, и я все-таки выберу какой-нибудь фреймворк.
А, возможно, всё это лишнее, и проще обойтись без этого: писать прямые импорты и не создавать лишних абстрактных интерфейсов.
А какой у вас опыт с управлением зависимостями в питоне? И вообще — нужно ли это, или я изобретаю проблему из воздуха?
Управление зависимостями в Python: сравнение подходов

Я пишу на питоне лет пять, из них последние три года — развиваю собственный проект. Большую часть этого пути мне помогает в этом моя команда. И с каждым релизом, с каждой новой фичей у нас все больше усилий уходит на то, чтобы проект не превращался в месиво из неподдерживаемого кода; мы боремся с циклическими импортами, взаимными зависимостями, выделяем переиспользуемые модули, перестраиваем структуру.
К сожалению, в Python-сообществе нет универсального понятия «хорошей архитектуры», есть только понятие «питоничности», поэтому архитектуру приходится придумывать самим. Под катом — лонгрид с размышлениями об архитектуре и в первую очередь — об управлении зависимостями применимо к Python.
django.setup()
Начну с вопроса джангистам. Часто ли вы пишете вот эти две строчки?
С этого нужно начать файл, если вы хотите поработать с объектами django, не запуская сам вебсервер django. Это касается и моделей, и инструментов работы со временем ( django.utils.timezone ), и урлов ( django.urls.reverse ), и многого другого. Если этого не сделать, то вы получите ошибку:
Я постоянно пишу эти две строчки. Я большой любитель кода «на выброс»; мне нравится создать отдельный .py -файл, покрутить в нем какие-то вещи, разобраться в них — а потом встроить в проект.
И меня очень раздражает этот постоянный django.setup() . Во-первых, устаешь это везде повторять; а, во-вторых, инициализация django занимает несколько секунд (у нас большой монолит), и, когда перезапускаешь один и тот же файл 10, 20, 100 раз — это просто замедляет разработку.
Как избавиться от django.setup() ? Нужно писать код, который по минимуму зависит от django.
Например, если мы пишем некий клиент внешнего API, то можно сделать его зависимым от django:
а можно — независимым от django:
Во втором случае конструктор более громоздкий, зато любые манипуляции с этим классом можно делать, не загружая всю джанговскую машинерию.
Тесты тоже становятся проще. Как тестировать компонент, который зависит от настроек django.conf.settings ? Только замокав их декоратором @override_settings . А если компонент ни от чего не зависит, то и мокать будет нечего: передал параметры в конструктор — и погнали.
Управление зависимостями
История с зависимостью от django — это наиболее яркий пример проблемы, с которой я сталкиваюсь каждый день: проблемы управления зависимостями в python — и в целом выстраивания архитектуры python-приложений.
Отношение к управлению зависимостями в Python-сообществе неоднозначное. Можно выделить три основных лагеря:
-
Питон — гибкий язык. Пишем как хотим, зависим от чего хотим. Не стесняемся циклических зависимостей, подмены атрибутов у классов в рантайме и т.д.
Brandon Rhodes, Dropbox: Hoist your IO.
Пример из доклада:
Недавно я прочел Clean Architecture — и, кажется, понял, в чем ценность внедрения зависимостей в питоне и как его можно реализовать. Я увидел это на примере своего собственного проекта. Вкратце — это защита кода от поломок при изменениях другого кода.
Исходные данные
Есть API-клиент, который выполняет HTTP-запросы на сервис-укорачиватель:
И есть модуль, который укорачивает все ссылки в тексте. Для этого он использует API-клиент укорачивателя:
Логика выполнения кода живет в отдельном управляющем файле (назовем его контроллером):
Всё работает. Процессор парсит текст, укорачивает ссылки с помощью укорачивателя, возвращает результат. Зависимости выглядят вот так:

Проблема
Проблема вот какая: класс TextProcessor зависит от класса ShortenerClient — и сломается при изменении интерфейса ShortenerClient .
Как это может произойти?
Допустим, в нашем проекте мы решили отслеживать переходы по ссылкам и добавили в метод shorten_link аргумент callback_url . Этот аргумент означает адрес, на который должны приходить уведомления при переходе по той или иной ссылке.
Метод ShortenerClient.shorten_link стал выглядеть вот так:
И что получается? А получается то, что при попытке запуска мы получим ошибку:
То есть мы изменили укорачиватель, но сломался не он, а его клиент:

Ну и что такого? Ну сломался вызывающий файл, мы пошли и поправили его. В чем проблема-то?
Если это решается за минуту — пошли и поправили — то это, конечно, и не проблема вовсе. Если в классах мало кода и если вы поддерживаете их самостоятельно (это ваш сайд-проект, это два небольших класса одной подсистемы и тд) — то на этом можно остановиться.
Проблемы начинаются, когда:
- в вызывающем и вызываемом модулях много кода;
- поддержкой разных модулей занимаются разные люди/команды.
Еще интереснее — когда ваш модуль используется в нескольких местах, а не в одном; и ваша правка поломает код в куче файлов.
Поэтому задачу можно сформулировать так: как организовать код так, чтобы при изменении интерфейса ShortenerClient ломался сам ShortenerClient , а не его потребители (которых может быть много)?
Решение здесь такое:
- Потребители класса и сам класс должны договориться об общем интерфейсе. Этот интерфейс должен стать законом.
- Если класс перестанет соответствовать своему интерфейсу — это будут уже его проблемы, а не проблемы потребителей.
Замораживаем интерфейс
Как в питоне выглядит фиксация интерфейса? Это абстрактный класс:
Если теперь мы унаследуемся от этого класса и забудем реализовать какой-то метод — мы получим ошибку:
Но этого недостаточно. Абстрактный класс фиксирует только названия методов, но не их сигнатуру.
Нужен второй инструмент для проверки сигнатуры Этот второй инструмент — mypy . Он поможет проверить сигнатуры унаследованных методов. Для этого мы должны добавить в интерфейс аннотации:
Если теперь проверить этот код при помощи mypy , мы получим ошибку из-за лишнего аргумента callback_url :
Теперь у нас есть надежный способ зафиксировать интерфейс класса.
Инверсия зависимости
Отладив интерфейс, мы должны переместить его в другое место, чтобы окончательно устранить зависимость потребителя от файла shortener_client.py . Например, можно перетащить интерфейс прямо в потребителя — в файл с процессором TextProcessor :
И это изменит направление зависимости! Теперь интерфейсом взаимодействия владеет TextProcessor , и в результате ShortenerClient зависит от него, а не наоборот.

В простых словах можно описать суть нашего преобразования так:
- TextProcessor говорит: я процессор, и я занимаюсь преобразованием текста. Я не хочу ничего знать о механизме укорачивания: это не моё дело. Я хочу дернуть метод shorten_link , чтоб он мне всё укоротил. Поэтому будьте добры, передайте мне объект, который играет по моим правилам. Решения о способе взаимодействия принимаю я, а не он.
- ShortenerClient говорит: похоже, я не могу существовать в вакууме, и от меня требуют определенного поведения. Пойду спрошу у TextProcessor , чему мне нужно соответствовать, чтобы не ломаться.
Несколько потребителей
Если же укорачиванием ссылок пользуются несколько модулей, то интерфейс нужно положить не в одного из них, а в какой-то отдельный файл, который находится «над» остальными файлами, выше по иерархии:

Управляющий компонент
Если потребители не импортируют ShortenerClient , то кто все-таки его импортирует и создает объект класса? Это должен быть управляющий компонент — в нашем случае это controller.py .
Самый простой подход — это прямолинейное внедрение зависимостей, Dependency Injection «в лоб». Создаём объекты в вызывающем коде, передаем один объект в другой. Профит.
Питоничный подход
Считается, что более «питоничный» подход — это Dependency Injection через наследование.
Чтобы адаптировать код под этот стиль, нужно немного поменять TextProcessor , сделав его наследуемым:
И затем, в вызывающем коде, унаследовать его:
Второй пример повсеместно встречается в популярных фреймворках:
- В Django мы постоянно наследуемся. Мы переопределяем методы Class-based вьюх, моделей, форм; иначе говоря, инджектим свои зависимости в уже отлаженную работу фреймворка.
- В DRF — то же самое. Мы расширяем вьюсеты, сериализаторы, пермишены.
- И так далее. Примеров масса.
Развитие питоничного подхода
В бизнес-логике обычно больше двух компонентов. Предположим, что наш TextProcessor , — это не самостоятельный класс, а лишь один из элементов пайплайна TextPipeline , который обрабатывает текст и шлет его на почту:
Если мы хотим изолировать TextPipeline от используемых классов, мы должны проделать такую же процедуру, что и раньше:
- класс TextPipeline будет декларировать интерфейсы для используемых компонентов;
- используемые компоненты будут вынуждены соответствовать этим интерфейсам;
- некий внешний код будет собирать все воедино и запускать.

Но как теперь будет выглядеть код сборки этих зависимостей?
Заметили? Мы сначала наследуем класс TextProcessor , чтобы вставить в него ShortenerClient , а потом наследуем TextPipeline , чтобы вставить в него наш переопределенный TextProcessor (а также Mailer ). У нас появляется несколько уровней последовательного переопределения. Уже сложновато.
Почему же все фреймворки организованы именно таким образом? Да потому, что это подходит только для фреймворков.
- Все уровни фреймворка четко определены, и их количество ограничено. Например, в Django можно переопределить FormField , чтобы вставить его в переопределение формы Form , чтобы вставить форму в переопределение View . Всё. Три уровня.
- Каждый фреймворк служит одной задаче. Эта задача четко определена.
- У каждого фреймворка есть подробная документация, в которой описано, как и что наследовать; что и с чем комбинировать.
Вернемся к подходу «в лоб»
На нескольких уровнях сложности выигрывает простой подход. Он выглядит проще — и его легче менять, когда меняется логика.
Но, когда количество уровней логики возрастает, даже такой подход становится неудобным. Нам приходится в императивном ключе инициировать кучу классов, передавая их друг в друга. Хочется избежать множества уровней вложенности.
Попробуем еще один заход.
Глобальное хранилище инстансов
Попробуем создать некий глобальный словарь, в котором будут лежать инстансы нужных нам компонентов. И пусть эти компоненты достают друг друга через обращение к этому словарю.
Назовем его INSTANCE_DICT :
Трюк — в том, чтобы подложить в этот словарь наши объекты до того, как к ним обратятся. Это мы и сделаем в controller.py :
Плюсы работы через глобальный словарь:
- никакой подкапотной магии и лишних DI-фреймворков;
- плоский список зависимостей, в котором не нужно управлять вложенностью;
- все бонусы DI: простое тестирование, независимость, защита компонентов от поломок при изменениях других компонентов.
Возможно, в какой-то момент мне станет этого мало, и я все-таки выберу какой-нибудь фреймворк.
А, возможно, всё это лишнее, и проще обойтись без этого: писать прямые импорты и не создавать лишних абстрактных интерфейсов.
А какой у вас опыт с управлением зависимостями в питоне? И вообще — нужно ли это, или я изобретаю проблему из воздуха?
Менеджер пакетов pip: разбираемся с установкой дополнительных библиотек в Python

Если вы работаете с языком программирования Python, то не раз сталкивались с утилитой pip . Общение с этим пакетом для большинства начинающих программистов ограничивается командой install . Однако возможности менеджера пакетов существенно шире.
1. Предназначение менеджера пакетов pip
Ведь не все пакеты нужны в повседневной практике или отдельном проекте, да и места они занимают не мало. Для этих целей создан удаленный репозиторий модулей https://pypi.org/ , в котором на сегодня имеется более 260 тыс. проектов на все случаи практики программирования. Вам не обязательно создавать код с нуля, так как под многие задачи уже имеется соответствующий пакет.
Работа с этим хранилищем расширений осуществляется через команду pip . Имеется и другой установщик easy_install , но он применяется существенно реже. Таким образом, пакетный менеджер pip необходим для установки, обновления, удаления и управления модулями языка Python.
2. Подготовительные мероприятия
Чтобы пользоваться возможностями пакетного менеджера pip, его необходимо установить. Если версия вашего Python выше 3.4 или 2.7.9 , то pip уже интегрирован в него. Использование более ранних версий языка не рекомендуется (вы теряете часть функционала). Другой способ установить pip (если вы его удалили случайно):
Проверить, что в вашем проекте или на ПК доступен pip , можно применяя следующую команду —version или -V :
Как видно из ответа, на данном ПК используется python версии 3.8 и pip версии 20.2.3 .
В некоторых случаях (актуально для пользователей Linux или macOS ) требуется применять команду pip3 (если в результате выполнения pip определяет, что у вас установлен python версии 2 по умолчанию). Это связано с тем, что на *nix системах присутствуют сразу обе версии языка.
Также если на вашем компьютере имеется несколько версий языка Python (например, 3.6 , 3.8 , 3.9 ), то менеджер пакетов может применяться отдельно для каждой из них:
После установки менеджера пакетов и определения его версии не следует забывать об его обновления до последнего издания. Для windows систем:
В результате выполнения команды мы получим сообщение об успешном обновлении pip до последнего релиза или сведения о том, что у нас уже установлена последняя версия.
3. Установка и удаление пакетов
Наиболее часто используемая команда в менеджере пакетов связана с непосредственной установкой необходимых модулей. Для примера установим библиотеку NumPy (позволяет эффективно работать с многомерными массивами и включает ряд соответствующих математических функций).
После выполнения команды данный модуль добавится в вашу библиотеку со всеми необходимыми дополнениями. Важно отметить, что будет загружена последняя версия NumPy . Бывают случаи, когда для проекта используется определенная версия пакета. Чтобы ничего не «сбилось», требуется установить именно этот релиз либо версию, которая не ниже определенной. Для этого применяются следующие команды:
При разработке сложных проектов может понадобиться установка большого количества модулей. Постоянно их скачивать из репозитория PyPi трудоемко. Для этого разработан способ загрузки пакетов локально. Они могут находиться в архивах ( *.tar.gz ) или специальных файлах с расширением .whl . Это удобно и в том случае, если нет доступа в интернет у выбранной машины, и вы заранее создали пакет со всеми необходимыми библиотеками.
Для примера запакуем модуль numpy в «колесо» ( wheel ) и установим его оттуда.
Вначале мы создали специальный локальный пакет NumPy и поместили его в текущую папку (о чем свидетельствует точка). В директории создался файл numpy-1.19.2-cp38-cp38-win32.whl . На его основании даже без интернета мы легко сможем установить данную библиотеку. Команда «—no-index» говорит о том, чтобы мы не искали модуль в репозитории PyPi , а —find-links принудительно указывает место расположения пакета. Когда речь идет о сотне пакетов, это очень удобно. Правда для этого необходимо освоить еще один инструмент: набор зависимостей (о нем – следующий раздел).
Рассмотрим вопрос удаления модулей. Если требуется удалить один пакет, то делается это по аналогии с установкой:
Для удаления нескольких модулей их можно перечислить через пробел или воспользоваться файлом requirements.txt . Чтобы при стирании библиотек постоянно не запрашивалось подтверждение от пользователя ( «введите Y для удаления или N для отмены» ), применяется ключ -y или —yes .
К слову, при установке нового пакета или его обновлении старая версия удаляется из библиотеки конкретного окружения.
Dependency tree of a Python Module
This is a python built-in module that can help us know the dependent packages but it shows all dependencies as a flat list, finding out which are the top-level packages and which packages do they depend on requires some effort. Let us see an example of how it works:
Type given below command on your command prompt:
Output:
pipdeptree utility:
One easy way of doing so is to use the pipdeptree utility. The pipdeptree works on the command line and shows the installed python packages in the form of a dependency tree.
This module does not come built-in with Python. To install it type the below command in the terminal.
This will install the latest version of pipdeptree which requires at least Python 2.7.
Now run this command on command prompt to get a dependency tree of all your Python modules.
Command:
Output:
Combining pipdeptree and freeze:
Let’s see what happens when we use pipdeptree and freeze altogether,
Command:
Output:
So, here we see that using pipdeptree along with freeze shows the output by combining the properties of both commands. So it looks like the output of pip freeze indicates which that package installed which another package, similar to pipdeptree but here indentation is used instead of a hyphen(-) to indicate tree.
Warnings in pipdeptree:
Commonly there occur two types of warnings while executing pipdeptree command, let us see them one by one.
1. Conflicting Dependencies: As the name suggests “conflicting dependency”, so is its relevance. Sometimes there is/are package(s) that are specified as a dependency of multiple packages with a different version, in this situation possible conflicting dependency warning arises. So, any package that’s specified as a dependency of multiple packages with a different version is considered as a possible conflicting dependency.
pipdeptree by default warns about possible conflicting dependencies.
Let us see one more example of pipdeptree:
Command:
Output:
pip doesn’t have a true dependency resolution yet. The warning is printed to stderr (Standard error) instead of stdout (Standard Output). To completely silence this warning use the -w silence or –warn silence flag It can also be made mode strict with –warn fail in which case the command will not only print the warnings to stderr but also exit with a non-zero status code. This could be useful if you want to fit this tool into your CI pipeline.
Note: The –warn flag was added in version 0.6.0. For older version, use –nowarn flag.
2. Circular Dependencies: This dependency occurs when two packages depend on each other. Suppose, package A depends upon package B and package B depends upon package A.
For this let us see one more example:
Command:
Output:
Note: They are also printed to stderr and can be controlled using the –warn flag.
To find why a particular package is installed:
Now, we may sometimes want to know why a particular package is installed. Then we can use –reverse (or simply -r) flag for this. To find out what all packages require a particular package(s), it can be combined with –packages
flag as shown in following example:
Command:
Output:
Using pipdeptree to write requirements.txt file:
If you wish to track only the top-level packages in your requirements.txt file, it’s possible to do so using pipdeptree by grep-ing only the top-level lines from the output,
Command:
Output:
There is a problem here though. The output doesn’t mention anything about Lookupy being installed as an editable package (refer to the output of pip freeze above) and information about its source is lost. To fix this, pipdeptree must be run with a -f or –freeze flag
Command:
Output:
Command:
The freeze flag will also not output the hyphens for child dependencies, so you could dump the complete output of pipdeptree -f to the requirements.txt file making the file human-friendly (due to indentations) as well as pip-friendly. (Take care of duplicate dependencies though)
Using pipdeptree with External tools:
pipdeptree uses flag –json to show output in JSON representation, as shown below:
Command:
Output:
Note: –json will output a flat list of all packages with their immediate dependencies. To obtain nested JSON, use –-json-tree (added in version 0.11.0).
Введение в инъекцию зависимости в Python
Мотивация: После бессмысленного создания проектов и написания кода, который реализует что-нибудь, но никогда … Текс с DesignPattern, Python, Advanced, зависимости.
- Автор записи
Мотивация: после бессмысленного создания проектов и написания кода, который реализует что-нибудь, но никогда не имел никакого смысла от глаз птичьего глаза почти 11 лет, пришло время узнать что-то, что поможет мне понять узоры дизайна и написать код, который на самом деле имел смысл и выглядел эстетически (принося в Instagram Vibes в dev.to ). Недавно наступил в зависимости от инъекции на зависимости в Python на работе и думал о том, чтобы дать ему попробовать.
Что такое впрыск зависимости (ди) в Python?
DI – обычный дизайн-шаблон, используемый в самых современных (и не так современных) языках программирования. Тем не менее, это остается недоиспользованной функциональностью в Python. Многие люди говорят за и против использования Di в Python, который является темой на другой день.
Как использовать инъекцию зависимости (DI) в Python?
Зависимость и инжектор ( Документы ) – это библиотека Python, которая предоставляет рамку, которая позволяет вам реализовать DI и IOC в Python.
НАЧИНАЯ
Два ключевых компонента Зависимость и инжектор являются
Поставщики Создайте объект и ввести зависимости. Ресурс , Фабрика , Конфигурация , Singleton , Callable и т. Д. И т. Д. Некоторые из типов провайдеров, и это не исчерпывающий список, но мы будем поддерживать нашу дискуссию ограничены вокруг этих типов, чтобы оправдать объем статьи.
Контейнер это просто коллекция Поставщики Отказ
Держите в разуме ниже, так как это будет единственная мантра, которая будет жить при реализации Di:
Каждый провайдер Callable. Всякий раз, когда вам нужен объект определенного типа, просто позвоните соответствующим провайдере, как вы позвоните в функцию.
Достаточно с теорией и сейчас,
Давайте приступим наши руки:
Начнем с помощью Importing Контейнеры и Поставщики от Зависимость_injector упаковка.
Мы создадим два класса Школа и Студент Отказ А Студент идет к определенному Школа Итак, У студента есть зависимость в школе.
Давайте посмотрим на атрибуты этих классов:
Школа:
- название школы : ул – У каждой школы есть имя, очевидно!
- Город : ул . | — Где ты учишься? Школа по имени в доска
- : ул — Какое образование следуют ваша школа? C.B.BE.? I.cs.s.se. или государственный совет?
Ученик:
- название : ул . | — Опять же, у каждого студента есть имя, очевидно! возраст
- : int — Сколько тебе лет? Я лет. Школа
- : Школа — в какой школе вы учитесь? Я учиюсь на класс
- : int — Какую оценку вы находитесь? Я в
Для создания зависимостей мы создадим класс под названием Контейнер
Здесь мы создали 2 поставщиков, Школа и Студент Отказ Чтобы инициализировать заводского поставщика, мы используем провайдеры. Фабрика И предоставить класс в качестве первого аргумента и другие аргументы являются атрибутами класса, необходимые для инициализации класса. Поставщик Singleton работает так же, как и заводской поставщик с дополнительным ограничением, что он будет создать только один объект, который будет запоминаться во всей жизни приложения.
Поскольку многие студенты ходят в одну школу, мы установили эту связь, сделав студентов завод и школу Singleton.
С тех пор мы установили этот факт выше что Студент имеет зависимость на Школа Теперь мы создадим эту зависимость. Если вы внимательно следите, после создания Школа Зависимость вводится в студенту. Посмотрите близко и соблюдайте последний аргумент поставщика студента:
Вот как мы создаем и вводите зависимости. Теперь все, что нам нужно сделать, это создать один объект контейнера и проводит модули вместе, и оно позаботится обо всех основных создании объекта и их инъекциях на подходящих позициях. Вот как мы это делаем:
Теперь, когда нам нужно увидеть студента, мы называем функцию get_student () с помощью поставщика студента.
Вот какой код выглядит во всей полноте:
Когда вы запускаете код, это будет ваш ожидаемый выход:
Честно говоря, этот пример может быть легко реализован с использованием наследства. Но когда ваш код больше и больше с десятками и сотнями классов, отслеживание всех объектов и их положение в дереве наследования чрезвычайно сложно. Это где di приходит в руках.
Он выглядит немного подавляющим в начале, но как только вы его понимаете, он делает ваш код очень гибким для изменения и тестирования.
Я буду писать подробную статью с расширенным использованием DI в реальном использовании где-то в ближайшем будущем. Поэтому, если вы нашли эту статью полезную, ваши отзывы будут искренне оценены, чтобы я мог избежать подобных ловушек в предстоящих статьях






