Прямой ответ
Внедрение зависимостей (dependency injection) — передача объектов, необходимых классу, извне вместо их создания внутри. В Python оно не требует фреймворка: в большинстве случаев достаточно передачи через конструктор, а контейнеры нужны только при большом числе зависимостей.
Антипаттерн — создание зависимости внутри:
class OrderService:
def __init__(self):
self.db = psycopg.connect(os.environ["DATABASE_URL"]) # зависимость создана внутри
self.mailer = SmtpMailer()
Такой класс нельзя протестировать без реальной базы и почтового сервера, а замена реализации требует правки кода класса.
Внедрение через конструктор:
class OrderService:
def __init__(self, db: Connection, mailer: Mailer) -> None:
self.db = db
self.mailer = mailer
Зависимости объявлены в сигнатуре, поэтому они видны без чтения тела класса, а в тест передаётся подставка:
def test_order_sends_email():
mailer = FakeMailer()
service = OrderService(db=InMemoryDb(), mailer=mailer)
service.create_order(items)
assert mailer.sent == ["user@example.com"]
Подстановка — обычный объект с тем же интерфейсом, поэтому unittest.mock.patch не требуется; о патчинге и его границах — в статье про моки и паттерны.
Сборка зависимостей выполняется в одном месте — в точке входа приложения:
def build_app(config: Config) -> FastAPI:
pool = ConnectionPool(config.database_url)
mailer = SmtpMailer(config.smtp)
orders = OrderService(db=pool, mailer=mailer)
...
Для веб-приложений механизм внедрения часто встроен в фреймворк: в FastAPI это Depends, который создаёт зависимость на запрос и передаёт её в обработчик; о механизме — в статье про зависимости FastAPI.
Практика: применение внедрения зависимостей
1. Передавайте зависимости через конструктор. Это минимальный и достаточный механизм для большинства классов.
2. Избегайте глобальных синглтонов. Модуль с глобальным экземпляром базы данных связывает все модули между собой и делает тесты зависимыми от порядка выполнения.
3. Собирайте зависимости в одном месте. Точка входа создаёт объекты и передаёт их дальше; о структуре — в статье про компоновку проекта.
4. Описывайте зависимости интерфейсом. Аннотация mailer: Mailer вместо mailer: SmtpMailer позволяет подменить реализацию без изменения класса.
5. Не внедряйте то, что не меняется. Функция форматирования даты или чистая утилита не требуют внедрения — оно добавляет сложность без пользы.
6. Используйте фабрики для зависимостей с состоянием на запрос. Соединение с базой создаётся на запрос и закрывается после него, поэтому внедряется фабрика, а не экземпляр.
7. Проверяйте удобство тестирования. Если для теста требуется патчить внутренние атрибуты, зависимости не внедрены.
8. Внедряйте время и случайность. Обращение к текущему времени и генератору случайных чисел внутри класса делает тесты недетерминированными. Передача источника времени как зависимости устраняет проблему без патчинга модуля.
9. Разделяйте конфигурацию и зависимости. Объект конфигурации передаётся в точку сборки, а сервисы получают готовые объекты. Передача конфига в каждый класс связывает их с форматом настроек.
10. Фиксируйте жизненный цикл. Зависимости, живущие на всё приложение, и создаваемые на запрос, требуют разного подхода к освобождению ресурсов. Смешение жизненных циклов приводит к утечке соединений.
Рекомендации:
- Не начинайте с контейнера. Библиотека контейнеров оправдана при десятках зависимостей и нескольких конфигурациях, а не в проекте из десяти модулей.
- Ограничьте число параметров конструктора. Более пяти-шести зависимостей указывает на то, что класс делает слишком много.
- Различайте внедрение и патчинг. Внедрение проектируется заранее, патчинг — обходной путь для кода, который не был спроектирован под замену.
- Документируйте точку сборки. Новый разработчик должен понимать, где создаются зависимости и как их подменить в тесте.
- Избегайте циклических зависимостей. Если два класса требуют друг друга, граница между ними выбрана неверно.
Практический сценарий: ускорение тестов с 9 минут до 40 секунд через внедрение зависимостей
Контекст: набор тестов выполнялся 9 минут, из которых большую часть занимали подключение к реальной базе и ожидание почтового сервера.
- Диагностика. Сервисы создавали подключения внутри конструкторов, читая адрес из переменных окружения. Тесты использовали реальную базу, а для изоляции очищали таблицы перед каждым тестом.
- Причина. Отсутствие внедрения зависимостей: замена реализации требовала либо патчинга внутренних атрибутов, либо реальной инфраструктуры. Патчинг применялся выборочно и ломался при изменении кода.
- Действия. (а) Конструкторы сервисов перевели на приём зависимостей как параметров; (б) добавили фабрику подключения, чтобы тесты передавали своё; (в) для тестов создали реализации с хранением в памяти, поддерживающие тот же интерфейс; (г) патчинг внутренних атрибутов убрали, заменив передачей подставок; (д) точку сборки вынесли в отдельный модуль, используемый и приложением, и тестами.
- Результат. Время выполнения сократилось до 40 секунд, а число падений из-за состояния базы исчезло. Интеграционные тесты с реальной базой остались, но выполняются отдельным набором.
Вывод: внедрение зависимостей в Python решается передачей через конструктор и одной точкой сборки. Оно не требует фреймворка, но меняет тестируемость: подстановка вместо патчинга устойчивее к изменениям кода и не зависит от порядка выполнения тестов.
FAQ
Нужен ли фреймворк для внедрения зависимостей в Python? В большинстве случаев нет. Передачи через конструктор и одной функции сборки достаточно; контейнер оправдан при большом числе зависимостей.
Чем внедрение отличается от патчинга в тестах? Внедрение делает зависимость явной в сигнатуре, а подмена выполняется передачей объекта. Патчинг изменяет внутреннее состояние и ломается при рефакторинге.
Что внедрять, а что нет? Внедряются объекты с внешними эффектами: база, сеть, файловая система, часы. Чистые функции и утилиты внедрения не требуют.
Как внедрять зависимость с жизненным циклом на запрос? Через фабрику либо механизм фреймворка, создающий зависимость на запрос и освобождающий ресурсы после его завершения.
Источники: Python docs (dataclasses), FastAPI docs (Dependencies), pytest docs (fixtures) — ссылки в шапке.