Iniekcja zależności: sposób Pythona
Wzorce DI w Pythonie dla czystego, testowalnego kodu
Iniekcja zależności (DI) to fundamentalny wzorzec projektowy, który promuje czysty, testowalny i utrzyjmalny kod w aplikacjach Pythona.
Nie ważne, czy tworzysz API REST z FastAPI, czy implementujesz testy jednostkowe, czy pracujesz z funkcjami AWS Lambda, zrozumienie iniekcji zależności znacząco poprawi jakość Twojego kodu.

Co to jest iniekcja zależności?
Iniekcja zależności to wzorzec projektowy, w którym komponenty otrzymują swoje zależności z zewnętrznych źródeł zamiast tworzyć je wewnętrznie. Ten podejście rozdzielają komponenty, co sprawia, że Twój kod jest bardziej modułowy, testowalny i utrzyjmalny.
W Pythonie, iniekcja zależności jest szczególnie potężna ze względu na dynamiczną naturę języka oraz wsparcie dla protokołów, klas bazowych i typowania „duck typing”. Flexybilność Pythona oznacza, że możesz implementować wzorce DI bez ciężkich frameworków, choć frameworki są dostępne, gdy są potrzebne.
Dlaczego używać iniekcji zależności w Pythonie?
Poprawiona testowalność: Poprzez iniekcję zależności możesz łatwo zastąpić rzeczywiste implementacje mockami lub testowymi podwójnymi. To umożliwia napisanie testów jednostkowych, które są szybkie, izolowane i nie wymagają usług zewnętrznych, takich jak bazy danych lub API. Gdy piszesz kompleksowe testy jednostkowe, iniekcja zależności sprawia, że zamiana rzeczywistych zależności na testowe podwójne jest trywialna.
Lepsza utrzyjmalność: Zależności stają się jawne w Twoim kodzie. Gdy spojrzysz na konstruktor, od razu widzisz, co wymaga komponent. To sprawia, że kod jest łatwiejszy do zrozumienia i modyfikacji.
Słabsze sprzężenie: Komponenty zależą od abstrakcji (protokołów lub ABC), a nie od konkretnych implementacji. To oznacza, że możesz zmieniać implementacje bez wpływu na kod zależny.
Flexybilność: Możesz konfigurować różne implementacje dla różnych środowisk (rozwijanie, testowanie, produkcja) bez zmiany logiki biznesowej. To jest szczególnie przydatne, gdy wdrażasz aplikacje Pythona na różnych platformach, czy to AWS Lambda czy tradycyjne serwery.
Iniekcja przez konstruktor: Pythonowy sposób
Najczęstszy i najbardziej idiomiczny sposób implementacji iniekcji zależności w Pythonie to iniekcja przez konstruktor – przekazywanie zależności jako parametr w metodzie __init__.
Prosty przykład
Oto prosty przykład pokazujący iniekcję przez konstruktor:
from typing import Protocol
from abc import ABC, abstractmethod
# Zdefiniuj protokół dla repozytorium
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Usługa zależy od protokołu repozytorium
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)
Ten wzorzec czyni jasnym, że UserService wymaga UserRepository. Nie można utworzyć UserService bez dostarczenia repozytorium, co zapobiega błędom w czasie działania z brakującymi zależnościami.
Wiele zależności
Gdy komponent ma wiele zależności, po prostu dodaj je jako parametry konstruktora:
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
Użycie Protokołów i Klas Bazowych
Jednym z kluczowych zasad przy implementacji iniekcji zależności jest Zasada Odwrócenia Zależności (DIP): moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu; oba powinny zależeć od abstrakcji.
W Pythonie możesz definiować abstrakcje za pomocą Protokołów (strukturalnego typowania) lub Klas Bazowych (ABC) (typowania nazwowego).
Protokoły (Python 3.8+)
Protokoły używają strukturalnego typowania – jeśli obiekt ma wymagane metody, spełnia protokół:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Każdy klasa z metodą process_payment spełnia ten protokół
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logika karty kredytowej
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logika PayPal
return True
# Usługa akceptuje dowolny PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Klasy Bazowe
ABCs używają typowania nazwowego – klasy muszą jawne dziedziczyć z 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
Kiedy używać Protokołów vs ABCs: Używaj Protokołów, gdy chcesz strukturalne typowanie i elastyczność. Używaj ABC, gdy musisz wymusić hierarchię dziedziczenia lub dostarczyć domyślnych implementacji.
Przykład z życia: Abstrakcja bazy danych
Gdy pracujesz z bazami danych w aplikacjach Pythona, często musisz abstrahować operacje bazy danych. Oto jak iniekcja zależności pomaga:
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]:
...
# Repozytorium zależy od abstrakcji
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
Ten wzorzec umożliwia wymianę implementacji bazy danych (PostgreSQL, SQLite, MongoDB) bez zmiany kodu repozytorium.
Wzorzec Korzenia Kompozycji
Korzeń Kompozycji to miejsce, w którym łączysz wszystkie zależności w punkcie wejścia aplikacji (zwykle main.py lub fabryce aplikacji). To centralizuje konfigurację zależności i czyni graf zależności jawny.
def create_app() -> FastAPI:
app = FastAPI()
# Inicjalizacja zależności infrastruktury
db = init_database()
logger = init_logger()
# Inicjalizacja repozytoriów
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inicjalizacja usług z zależnościami
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Inicjalizacja obsługi HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Podłączenie tras
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Ten podejście czyni jasnym, jak jest zbudowana Twoja aplikacja i skąd pochodzą zależności. Jest szczególnie wartościowe, gdy tworzysz aplikacje zgodne z zasadami architektury czystej, gdzie musisz koordynować wiele warstw zależności.
Frameworki Iniekcji Zależności
Dla większych aplikacji z złożonym grafem zależności, zarządzanie zależnościami ręcznie może stać się uciążliwe. Python ma kilka frameworków DI, które mogą pomóc:
Dependency Injector
Dependency Injector to popularny framework, który oferuje podejście oparte na kontenerze do iniekcji zależności.
Instalacja:
pip install dependency-injector
Przykład:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Konfiguracja
config = providers.Configuration()
# Baza danych
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repozytoria
user_repository = providers.Factory(
UserRepository,
db=db
)
# Usługi
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Użycie
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector to lekki biblioteka zainspirowana Google Guice, skupiająca się na prostocie.
Instalacja:
pip install injector
Przykład:
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)
Kiedy używać frameworków
Użyj frameworku, gdy:
- Twój graf zależności jest złożony z wielu wzajemnie zależnych komponentów
- Masz wiele implementacji tej samej interfejsu, które należy wybrać na podstawie konfiguracji
- Chcesz automatyczne rozwiązywanie zależności
- Tworzysz dużą aplikację, w której ręczne podpinanie zależności staje się błędne
Zostań przy ręcznej DI, gdy:
- Twoja aplikacja jest mała do średniej wielkości
- Graf zależności jest prosty i łatwy do śledzenia
- Chcesz zachować zależności minimalne i jawne
- Preferujesz jawny kod nad magią frameworku
Testowanie z Iniekcją Zależności
Jednym z głównych korzyści z iniekcji zależności jest poprawiona testowalność. Oto jak DI ułatwia testowanie:
Przykład testu jednostkowego
from unittest.mock import Mock
import pytest
# Przykładowa implementacja do testowania
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
# Test z użyciem mocka
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"
Ten test działa szybko, nie wymaga bazy danych i testuje logikę biznesową izolowanie. Gdy pracujesz z testowaniem jednostkowym w Pythonie, iniekcja zależności ułatwia tworzenie testowych podwójnych i weryfikację interakcji.
Użycie pytest Fixtures
Fixtury pytest działają bardzo dobrze z iniekcją zależności:
@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"
Powszechne wzorce i najlepsze praktyki
1. Używaj segregacji interfejsów
Każdy protokół i interfejs powinien być mały i skupiony na tym, czego klient naprawdę potrzebuje:
# Dobrze: Klient potrzebuje tylko odczytywać użytkowników
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Odrębny interfejs do zapisu
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Waliduj zależności w konstruktorach
Konstruktory powinny walidować zależności i wznosić wyraźne błędy, jeśli inicjalizacja nie powiedzie się:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("repozytorium użytkowników nie może być None")
self.repo = repo
3. Używaj wskazówek typu
Wskazówki typu czynią zależności jawne i pomagają w obsłudze IDE oraz statycznym sprawdzaniu typów:
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. Unikaj nadmiernej iniekcji
Nie iniekcjonuj zależności, które są naprawdę wewnętrznymi szczegółami implementacji. Jeśli komponent tworzy i zarządza swoimi własnymi pomocniczymi obiektami, to jest w porządku:
# Dobrze: Wewnętrzny pomocnik nie wymaga iniekcji
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Wewnętrzny cache - nie wymaga iniekcji
self._cache: dict[int, User] = {}
5. Dokumentuj zależności
Używaj docstringów do dokumentowania, dlaczego są potrzebne zależności i jakie są ograniczenia:
class UserService:
"""UserService obsługuje logikę biznesową związana z użytkownikami.
Args:
repo: UserRepository do dostępu do danych. Musi być bezpieczny dla wątków,
jeśli jest używany w kontekście współbieżnym.
logger: Logger do śledzenia błędów i debugowania.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Iniekcja zależności w FastAPI
FastAPI ma wbudowaną obsługę iniekcji zależności poprzez mechanizm 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
System iniekcji zależności FastAPI obsługuje graf zależności automatycznie, co ułatwia budowanie czystych, utrzyjmalnych API.
Kiedy NIE używać Iniekcji Zależności
Iniekcja zależności to potężny narzędzie, ale nie zawsze jest konieczna:
Pomiń DI w przypadku:
- Prostych obiektów wartościowych lub klas danych
- Wewnętrznych funkcji pomocniczych lub narzędzi
- Skryptów jednorazowych lub małych narzędzi
- Kiedy bezpośrednie tworzenie instancji jest wyraźniejsze i prostsze
Przykład kiedy NIE używać DI:
# Prosta klasa danych - nie ma potrzeby DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Prosty narzędzie - nie ma potrzeby DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integracja z ekosystemem Pythona
Iniekcja zależności działa płynnie z innymi wzorcami i narzędziami Pythona. Gdy budujesz aplikacje, które używają pakietów Pythona lub frameworków testowania jednostkowego, możesz wstrzyknąć te usługi do Twojej logiki biznesowej:
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"Generated report {report_id}")
return pdf
To umożliwia wymianę implementacji lub użycie mocków podczas testowania.
Asynchroniczna Iniekcja Zależności
Sytaksa async/await w Pythonie działa dobrze z iniekcją zależności:
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]
Podsumowanie
Iniekcja zależności to fundament pisania utrzyjmalnego, testowalnego kodu w Pythonie. Przyjmując wzorce omawiane w tym artykule – iniekcja przez konstruktor, projekt oparty na protokołach oraz wzorzec korzenia kompozycji – utworzysz aplikacje, które są łatwiejsze do zrozumienia, testowania i modyfikacji.
Zacznij od ręcznej iniekcji przez konstruktor dla aplikacji małych do średnich, a rozważ frameworki takie jak dependency-injector lub injector, gdy graf zależności rośnie. Pamiętaj, że celem jest przejrzystość i testowalność, a nie złożoność dla własnej woli.
Dla więcej zasobów związanych z rozwojem w Pythonie, sprawdź nasz Python Cheatsheet dla szybkiego odniesienia do składni Pythona i typowych wzorców.
Przydatne linki
- Python Cheatsheet
- Testowanie jednostkowe w Pythonie
- Wzorce projektowe w Pythonie dla architektury czystej
- Strukturalny Output – LLMs na Ollama z Qwen3 – Python i Go
- Porównanie strukturalnego outputu w popularnych dostawcach LLM – OpenAI, Gemini, Anthropic, Mistral i AWS Bedrock
- Tworzenie dwu trybowego AWS Lambda w Pythonie i Terraformie
Zewnętrzne zasoby
- Iniekcja zależności w Pythonie – Real Python
- Jak iniekcja zależności w Pythonie poprawia strukturę kodu – Volito Digital
- Przewodnik po iniekcji zależności w Pythonie – DataCamp
- Dependency Injector – Dokumentacja oficjalna
- Injector – Lekkie framework do DI
- Zależności w FastAPI – Oficjalna Dokumentacja
- Zasady SOLID w Pythonie – Lexikon wzorców projektowych
- Protokoły i strukturalne typowanie w Pythonie – PEP 544