Внедрение зависимостей в Python
Шаблоны проектирования DI для Python: чистый и тестируемый код
Внедрение зависимостей (DI) — это фундаментальный шаблон проектирования, который способствует созданию чистого, тестируемого и поддерживаемого кода в приложениях на Python.
Будь то создание REST API с FastAPI, реализация модульных тестов или работа с функциями AWS Lambda, понимание внедрения зависимостей значительно улучшит качество вашего кода.

Что такое внедрение зависимостей?
Внедрение зависимостей — это шаблон проектирования, при котором компоненты получают свои зависимости из внешних источников, а не создают их внутри. Этот подход делает компоненты более модульными, тестируемыми и поддерживаемыми.
В Python внедрение зависимостей особенно мощное благодаря динамической природе языка и поддержке протоколов, абстрактных базовых классов и утиной типизации. Гибкость Python позволяет реализовывать шаблоны DI без тяжелых фреймворков, хотя фреймворки доступны при необходимости.
Почему использовать внедрение зависимостей в Python?
Улучшенная тестируемость: Внедряя зависимости, вы можете легко заменять реальные реализации на моки или тестовые двойники. Это позволяет писать модульные тесты, которые быстрые, изолированные и не требуют внешних сервисов, таких как базы данных или API. При написании комплексных модульных тестов, внедрение зависимостей делает тривиальной замену реальных зависимостей на тестовые двойники.
Лучшая поддерживаемость: Зависимости становятся явными в вашем коде. Когда вы смотрите на конструктор, вы сразу видите, что требуется компоненту. Это делает кодовую базу легче для понимания и модификации.
Слабая связанность: Компоненты зависят от абстракций (протоколов или ABC) вместо конкретных реализаций. Это означает, что вы можете изменять реализации без влияния на зависимый код.
Гибкость: Вы можете настроить разные реализации для разных сред (разработка, тестирование, продакшн) без изменения бизнес-логики. Это особенно полезно при развертывании приложений Python на разных платформах, будь то AWS Lambda или традиционные серверы.
Внедрение через конструктор: Python-стиль
Самый распространенный и идиоматический способ реализации внедрения зависимостей в Python — это внедрение через конструктор, то есть принятие зависимостей в качестве параметров в методе __init__.
Базовый пример
Вот простой пример, демонстрирующий внедрение через конструктор:
from typing import Protocol
from abc import ABC, abstractmethod
# Определяем протокол для репозитория
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Сервис зависит от протокола репозитория
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int) -> 'User | None':
return self.repo.find_by_id(user_id)
Этот шаблон делает понятным, что UserService требует UserRepository. Вы не можете создать UserService без предоставления репозитория, что предотвращает ошибки времени выполнения из-за отсутствия зависимостей.
Множественные зависимости
Когда компонент имеет несколько зависимостей, просто добавьте их в качестве параметров конструктора:
class EmailService(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
class Logger(Protocol):
def info(self, msg: str) -> None:
...
def error(self, msg: str, err: Exception) -> None:
...
class OrderService:
def __init__(
self,
repo: OrderRepository,
email_svc: EmailService,
logger: Logger,
payment_svc: PaymentService,
):
self.repo = repo
self.email_svc = email_svc
self.logger = logger
self.payment_svc = payment_svc
Использование протоколов и абстрактных базовых классов
Одним из ключевых принципов при реализации внедрения зависимостей является Принцип инверсии зависимостей (DIP): высокоуровневые модули не должны зависеть от низкоуровневых модулей; оба должны зависеть от абстракций.
В Python вы можете определять абстракции с использованием либо Протоколов (структурная типизация), либо Абстрактных базовых классов (ABC) (номинальная типизация).
Протоколы (Python 3.8+)
Протоколы используют структурную типизацию — если объект имеет требуемые методы, он удовлетворяет протоколу:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Любой класс с методом process_payment удовлетворяет этому протоколу
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Логика кредитной карты
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Логика PayPal
return True
# Сервис принимает любой PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Абстрактные базовые классы
ABC используют номинальную типизацию — классы должны явно наследоваться от ABC:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
return True
Когда использовать Протоколы или ABC: Используйте Протоколы, когда вам нужна структурная типизация и гибкость. Используйте ABC, когда вам нужно наследовать иерархии или предоставлять реализации по умолчанию.
Пример из реального мира: Абстракция базы данных
При работе с базами данных в приложениях Python вам часто нужно абстрагировать операции с базой данных. Вот как помогает внедрение зависимостей:
from typing import Protocol, Optional
from contextlib import contextmanager
class Database(Protocol):
@contextmanager
def transaction(self):
...
def execute(self, query: str, params: dict) -> None:
...
def fetch_one(self, query: str, params: dict) -> Optional[dict]:
...
# Репозиторий зависит от абстракции
class UserRepository:
def __init__(self, db: Database):
self.db = db
def find_by_id(self, user_id: int) -> Optional['User']:
result = self.db.fetch_one(
"SELECT * FROM users WHERE id = :id",
{"id": user_id}
)
if result:
return User(**result)
return None
Этот шаблон позволяет вам менять реализации баз данных (PostgreSQL, SQLite, MongoDB) без изменения кода репозитория.
Шаблон Composition Root
Composition Root — это место, где вы собираете все ваши зависимости в точке входа приложения (обычно main.py или ваша фабрика приложения). Это централизует настройку зависимостей и делает граф зависимостей явным.
def create_app() -> FastAPI:
app = FastAPI()
# Инициализация зависимостей инфраструктуры
db = init_database()
logger = init_logger()
# Инициализация репозиториев
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Инициализация сервисов с зависимостями
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Инициализация HTTP-хендлеров
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Подключение маршрутов
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Этот подход делает понятным, как устроено ваше приложение и откуда берутся зависимости. Он особенно ценен при создании приложений, следующих принципам чистой архитектуры, где нужно координировать несколько уровней зависимостей.
Фреймворки внедрения зависимостей
Для крупных приложений с сложными графами зависимостей управление зависимостями вручную может стать громоздким. В Python есть несколько фреймворков DI, которые могут помочь:
Dependency Injector
Dependency Injector — это популярный фреймворк, предоставляющий контейнерный подход к внедрению зависимостей.
Установка:
pip install dependency-injector
Пример:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Конфигурация
config = providers.Configuration()
# База данных
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Репозитории
user_repository = providers.Factory(
UserRepository,
db=db
)
# Сервисы
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Использование
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector — это легковесная библиотека, вдохновленная Google’s Guice, с акцентом на простоту.
Установка:
pip install injector
Пример:
from injector import Injector, inject, Module, provider
class DatabaseModule(Module):
@provider
def provide_db(self) -> Database:
return Database(connection_string="...")
class UserModule(Module):
@inject
def __init__(self, repo: UserRepository):
self.repo = repo
injector = Injector([DatabaseModule()])
user_service = injector.get(UserService)
Когда использовать фреймворки
Используйте фреймворк, когда:
- Ваш граф зависимостей сложный с множеством взаимозависимых компонентов
- У вас есть несколько реализаций одного и того же интерфейса, которые нужно выбирать на основе конфигурации
- Вы хотите автоматическое разрешение зависимостей
- Вы создаете крупное приложение, где ручное подключение становится ошибкоопасным
Оставайтесь на ручном DI, когда:
- Ваше приложение небольшое или среднего размера
- Граф зависимостей простой и легко читаемый
- Вы хотите минимизировать зависимости и сделать их явными
- Вы предпочитаете явный код вместо магии фреймворка
Тестирование с внедрением зависимостей
Одним из основных преимуществ внедрения зависимостей является улучшенная тестируемость. Вот как DI упрощает тестирование:
Пример модульного тестирования
from unittest.mock import Mock
import pytest
# Моковая реализация для тестирования
class MockUserRepository:
def __init__(self):
self.users = {}
self.error = None
def find_by_id(self, user_id: int) -> Optional['User']:
if self.error:
raise self.error
return self.users.get(user_id)
def save(self, user: 'User') -> 'User':
if self.error:
raise self.error
self.users[user.id] = user
return user
# Тест с использованием мока
def test_user_service_get_user():
mock_repo = MockUserRepository()
mock_repo.users[1] = User(id=1, name="John", email="john@example.com")
service = UserService(mock_repo)
user = service.get_user(1)
assert user is not None
assert user.name == "John"
Этот тест выполняется быстро, не требует базы данных и тестирует бизнес-логику в изоляции. При работе с модульным тестированием в Python, внедрение зависимостей упрощает создание тестовых дублей и проверку взаимодействий.
Использование фикстур pytest
Фикстуры pytest отлично работают с внедрением зависимостей:
@pytest.fixture
def mock_user_repository():
return MockUserRepository()
@pytest.fixture
def user_service(mock_user_repository):
return UserService(mock_user_repository)
def test_user_service_get_user(user_service, mock_user_repository):
user = User(id=1, name="John", email="john@example.com")
mock_user_repository.users[1] = user
result = user_service.get_user(1)
assert result.name == "John"
Общие шаблоны и лучшие практики
1. Использование сегрегации интерфейсов
Сохраняйте протоколы и интерфейсы небольшими и сосредоточенными на том, что действительно нужно клиенту:
# Хорошо: Клиенту нужно только чтение пользователей
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Отдельный интерфейс для записи
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Проверка зависимостей в конструкторах
Конструкторы должны проверять зависимости и выдавать четкие ошибки, если инициализация не удалась:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("репозиторий пользователей не может быть None")
self.repo = repo
3. Использование аннотаций типов
Аннотации типов делают зависимости явными и помогают с поддержкой IDE и статической проверкой типов:
from typing import Protocol, Optional
class UserService:
def __init__(
self,
repo: UserRepository,
logger: Logger,
email_service: EmailService,
) -> None:
self.repo = repo
self.logger = logger
self.email_service = email_service
4. Избегайте чрезмерного внедрения
Не внедряйте зависимости, которые являются действительно внутренними деталями реализации. Если компонент создает и управляет своими собственными вспомогательными объектами, это нормально:
# Хорошо: Внутренний помощник не требует внедрения
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Внутренний кеш - не требует внедрения
self._cache: dict[int, User] = {}
5. Документируйте зависимости
Используйте строки документации для документирования того, почему нужны зависимости и какие есть ограничения:
class UserService:
"""UserService обрабатывает бизнес-логику, связанную с пользователями.
Аргументы:
repo: UserRepository для доступа к данным. Должен быть потокобезопасным
если используется в параллельных контекстах.
logger: Логгер для отслеживания ошибок и отладки.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Внедрение зависимостей с FastAPI
FastAPI имеет встроенную поддержку внедрения зависимостей через механизм Depends:
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
def get_user_repository() -> UserRepository:
db = get_database()
return UserRepository(db)
def get_user_service(
repo: Annotated[UserRepository, Depends(get_user_repository)]
) -> UserService:
return UserService(repo)
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)]
):
user = service.get_user(user_id)
if not user:
raise HTTPException(status_code=404)
return user
Система внедрения зависимостей FastAPI автоматически обрабатывает граф зависимостей, что упрощает создание чистых, поддерживаемых API.
Когда НЕ использовать внедрение зависимостей
Внедрение зависимостей - мощный инструмент, но оно не всегда необходимо:
Пропустите DI для:
- Простых объектов значений или классов данных
- Внутренних вспомогательных функций или утилит
- Разовых скриптов или небольших утилит
- Когда прямое инстанцирование понятнее и проще
Пример, когда НЕ использовать DI:
# Простой dataclass - не требует DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Простая утилита - не требует DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Интеграция с экосистемой Python
Внедрение зависимостей работает плавно с другими паттернами и инструментами Python. При создании приложений, использующих Python пакеты или фреймворки модульного тестирования, вы можете внедрять эти сервисы в свою бизнес-логику:
class ReportService:
def __init__(
self,
pdf_generator: PDFGenerator,
repo: ReportRepository,
logger: Logger,
):
self.pdf_generator = pdf_generator
self.repo = repo
self.logger = logger
def generate_report(self, report_id: int) -> bytes:
report_data = self.repo.get_by_id(report_id)
pdf = self.pdf_generator.generate(report_data)
self.logger.info(f"Сгенерирован отчет {report_id}")
return pdf
Это позволяет заменять реализации или использовать моки во время тестирования.
Асинхронное внедрение зависимостей
Синтаксис async/await Python хорошо работает с внедрением зависимостей:
from typing import Protocol
import asyncio
class AsyncUserRepository(Protocol):
async def find_by_id(self, user_id: int) -> Optional['User']:
...
async def save(self, user: 'User') -> 'User':
...
class AsyncUserService:
def __init__(self, repo: AsyncUserRepository):
self.repo = repo
async def get_user(self, user_id: int) -> Optional['User']:
return await self.repo.find_by_id(user_id)
async def get_users_batch(self, user_ids: list[int]) -> list['User']:
tasks = [self.repo.find_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Заключение
Внедрение зависимостей - это основа для написания поддерживаемого, тестируемого кода на Python. Следуя паттернам, описанным в этой статье - внедрение через конструктор, проектирование на основе протоколов и паттерн композиционного корня - вы создадите приложения, которые легче понять, протестировать и модифицировать.
Начните с ручного внедрения через конструктор для небольших и средних приложений, и рассмотрите фреймворки вроде dependency-injector или injector по мере роста графа зависимостей. Помните, что цель - ясность и тестируемость, а не сложность ради сложности.
Для дополнительных ресурсов по разработке на Python, ознакомьтесь с нашей Python Cheatsheet для быстрого доступа к синтаксису Python и общим паттернам.
Полезные ссылки
- Python Cheatsheet
- Модульное тестирование в Python
- Python Design Patterns for Clean Architecture
- Структурированный вывод - LLMs на Ollama с Qwen3 - Python и Go
- Сравнение структурированного вывода среди популярных поставщиков LLM - OpenAI, Gemini, Anthropic, Mistral и AWS Bedrock
- Создание двурежимной AWS Lambda с Python и Terraform
Внешние ресурсы
- Внедрение зависимостей в Python - Real Python
- Как внедрение зависимостей в Python улучшает структуру кода - Volito Digital
- Руководство по внедрению зависимостей в Python - DataCamp
- Dependency Injector - Официальная документация
- Injector - Легковесный фреймворк DI
- Зависимости FastAPI - Официальные документы
- Принципы SOLID в Python - Software Patterns Lexicon
- Протоколы Python и структурная подтипизация - PEP 544