Dependency Injection: En Python-approach
Pythons DI-mönster för ren, testbar kod
Dependency injection (DI) är en grundläggande designmönster som främjar ren, testbar och underhållbar kod i Python-program.
Vare sig du bygger REST API:er med FastAPI, implementerar enhets tester eller arbetar med AWS Lambda funktioner, så kommer förståelsen för dependency injection att betydligt förbättra kvaliteten på din kod.

Vad är dependency injection?
Dependency injection är ett designmönster där komponenter får sina beroenden från externa källor snarare än att skapa dem internt. Detta sätt gör att komponenterna är mer oberoende, vilket gör att din kod blir mer modulär, testbar och underhållbar.
I Python är dependency injection särskilt kraftfull eftersom språket har en dynamisk karaktär och stöd för protokoll, abstrakta basklasser och duck typing. Python:s flexibilitet innebär att du kan implementera DI-mönster utan tunga ramverk, även om ramverk finns tillgängliga när det behövs.
Varför använda dependency injection i Python?
Bättre testbarhet: Genom att injicera beroenden kan du enkelt ersätta verkliga implementeringar med mockar eller testdubbler. Detta gör att du kan skriva enhets tester som är snabba, isolerade och inte kräver externa tjänster som databaser eller API:er. När du skriver omfattande enhets tester, gör dependency injection det trivialt att byta ut verkliga beroenden mot testdubbler.
Bättre underhållbarhet: Beroenden blir explicita i din kod. När du tittar på en konstruktor, ser du direkt vad en komponent kräver. Detta gör att kodbasen blir lättare att förstå och modifiera.
Lösn koppling: Komponenter beroer på abstraktioner (protokoll eller ABCs) snarare än konkreta implementeringar. Detta innebär att du kan ändra implementeringar utan att påverka beroende kod.
Flexibilitet: Du kan konfigurera olika implementeringar för olika miljöer (utveckling, testning, produktion) utan att behöva ändra din affärslogik. Detta är särskilt användbart när du distribuerar Python-program till olika plattformar, oavsett om det är AWS Lambda eller traditionella servrar.
Konstruktörsinjektion: Python:s sätt
Det vanligaste och mest idiomatiska sättet att implementera dependency injection i Python är genom konstruktörsinjektion – att ta emot beroenden som parametrar i __init__-metoden.
Grundläggande exempel
Här är ett enkelt exempel som demonstrerar konstruktörsinjektion:
from typing import Protocol
from abc import ABC, abstractmethod
# Definiera ett protokoll för repository
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Service beroer på repository-protokollet
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)
Detta mönster gör det klart att UserService kräver en UserRepository. Du kan inte skapa en UserService utan att ange ett repository, vilket förhindrar körningsfel från saknade beroenden.
Flera beroenden
När en komponent har flera beroenden, lägg helt enkelt till dem som konstruktörsparametrar:
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
Använda protokoll och abstrakta basklasser
En av de viktigaste principerna när man implementerar dependency injection är Dependency Inversion Principle (DIP): högre nivåmoduler bör inte bero på lägre nivåmoduler; båda bör bero på abstraktioner.
I Python kan du definiera abstraktioner med hjälp av antingen protokoll (strukturtypning) eller abstrakta basklasser (ABCs) (namntypning).
Protokoll (Python 3.8+)
Protokoll använder strukturtypning – om ett objekt har de nödvändiga metoderna, uppfyller det protokollet:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Vare sig klassen har process_payment-metoden uppfyller den detta protokoll
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Kreditkortlogik
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# PayPal-logik
return True
# Service accepterar någon PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Abstrakta basklasser
ABCs använder namntypning – klasser måste explicit arvta från ABC:en:
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
När att använda protokoll vs ABCs: Använd protokoll när du vill ha strukturtypning och flexibilitet. Använd ABCs när du behöver tvinga arvshierarkier eller ge standardimplementeringar.
Verklighetsbaserat exempel: Databasabstraktion
När du arbetar med databaser i Python-program, kommer du ofta behöva abstrahera databasoperationer. Här är hur dependency injection hjälper:
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]:
...
# Repository beroer på abstraktionen
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
Detta mönster gör det möjligt att byta databasimplementeringar (PostgreSQL, SQLite, MongoDB) utan att behöva ändra din repository-kod.
Kompositionens rotmönster
Kompositionens rot är där du samlar ihop alla dina beroenden vid programmet:s startpunkt (ofta main.py eller din applikationsfabrik). Detta centraliserar beroendekonfigurationen och gör beroendegrafen explicit.
def create_app() -> FastAPI:
app = FastAPI()
# Initiera infrastrukturberoenden
db = init_database()
logger = init_logger()
# Initiera repositories
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Initiera tjänster med beroenden
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Initiera HTTP-hanterare
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Koppla upp rutter
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Detta sätt gör det tydligt hur din applikation är strukturerad och var beroenden kommer ifrån. Det är särskilt värdefullt när man bygger applikationer enligt rengörningsarkitekturprinciper, där du behöver koordinera flera nivåer av beroenden.
Dependency injectionramverk
För större applikationer med komplexa beroendegrafer kan det bli tråkigt att hantera beroenden manuellt. Python har flera DI-ramverk som kan hjälpa:
Dependency Injector
Dependency Injector är ett populärt ramverk som tillhandahåller en containerbaserad metod för dependency injection.
Installation:
pip install dependency-injector
Exempel:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Konfiguration
config = providers.Configuration()
# Databas
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repositories
user_repository = providers.Factory(
UserRepository,
db=db
)
# Tjänster
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Användning
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector är ett lättviktigt bibliotek inspirerat av Googles Guice, med fokus på enkelhet.
Installation:
pip install injector
Exempel:
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)
När att använda ramverk
Använd ett ramverk när:
- Din beroendegraf är komplett med många beroende komponenter
- Du har flera implementeringar av samma gränssnitt som behöver väljas utifrån konfiguration
- Du vill ha automatisk beroendelösning
- Du bygger en stor applikation där manuell anslutning blir felbenägen
Behåll manuell DI när:
- Din applikation är liten eller medelstor
- Din beroendegraf är enkel och lätt att följa
- Du vill hålla beroenden minimala och explicita
- Du föredrar explicit kod över ramverksmagi
Testning med dependency injection
En av de primära fördelarna med dependency injection är förbättrad testbarhet. Här är hur DI gör testning enklare:
Enhets testexempel
from unittest.mock import Mock
import pytest
# Mockimplementering för testning
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
# Testa med mock
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"
Detta test kör snabbt, kräver inte en databas och testar din affärslogik i isolation. När du arbetar med enhets test i Python, gör dependency injection det enkelt att skapa testdubbler och verifiera interaktioner.
Använda pytest fixtures
pytest fixtures fungerar utmärkt med dependency injection:
@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"
Vanliga mönster och bästa praxis
1. Använd gränssnittssegregering
Håll protokoll och gränssnitt små och fokuserade på vad klienten faktiskt behöver:
# Bra: Klienten behöver bara läsa användare
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Separat gränssnitt för skrivning
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Validera beroenden i konstruktörer
Konstruktörer bör validera beroenden och lyfta upp tydliga fel om initiering misslyckas:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("user repository kan inte vara None")
self.repo = repo
3. Använd typindikeringar
Typindikeringar gör beroenden explicita och hjälper till med IDE-stöd och statisk typkontroll:
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. Undvik överinjektion
Injicera inte beroenden som verkligen är inre implementeringsdetaljer. Om en komponent skapar och hanterar sina egna hjälpare, är det okej:
# Bra: Inre hjälpare behöver inte injektion
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Inre cache – behöver inte injektion
self._cache: dict[int, User] = {}
5. Dokumentera beroenden
Använd docstrings för att dokumentera varför beroenden behövs och några begränsningar:
class UserService:
"""UserService hanterar användarrelaterad affärslogik.
Args:
repo: UserRepository för dataåtkomst. Måste vara trådsäkert
om använd i samtidiga sammanhang.
logger: Logger för felspårning och felsökning.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Dependency injection med FastAPI
FastAPI har inbyggd stöd för dependency injection via dess Depends-mekanism:
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:s dependency injection-system hanterar beroendegrafen automatiskt, vilket gör det enkelt att bygga ren, underhållbar API:er.
När inte att använda dependency injection
Dependency injection är ett kraftfullt verktyg, men det är inte alltid nödvändigt:
Skipa DI för:
- Enkla värdeobjekt eller dataklasser
- Inre hjälpfunktioner eller verktyg
- Enkel skript eller små verktyg
- När direkt instansiering är tydligare och enklare
Exempel på när inte att använda DI:
# Enkel dataklass – ingen behov av DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Enkel verktyg – ingen behov av DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integration med Python-ekosystemet
Dependency injection fungerar sömlöst med andra Python-mönster och verktyg. När du bygger applikationer som använder Python-paket eller enhets testramverk, kan du injicera dessa tjänster i din affärslogik:
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
Detta gör det möjligt att byta ut implementeringar eller använda mockar under testning.
Asynkron dependency injection
Pythons async/await-syntax fungerar bra med dependency injection:
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]
Slutsats
Dependency injection är en grundpelare för att skriva underhållbar, testbar Python-kod. Genom att följa mönster som konstruktörsinjektion, protokollbaserad design och kompositionens rotmönster, kommer du att skapa program som är enklare att förstå, testa och modifiera.
Börja med manuell konstruktörsinjektion för små till medelstora applikationer, och överväg ramverk som dependency-injector eller injector när din beroendegraf växer. Kom ihåg att målet är tydlighet och testbarhet, inte komplexitet för sin egen skull.
För fler Python-utvecklingsresurser, se vårt Python Cheatsheet för snabb referens på Python-syntax och vanliga mönster.
Nytta länkar
- Python Cheatsheet
- Enhets test i Python
- Python Design Patterns för Clean Architecture
- Strukturerad utdata – LLMs på Ollama med Qwen3 – Python och Go
- Strukturerad utdatajämförelse över populära LLM-leverantörer – OpenAI, Gemini, Anthropic, Mistral och AWS Bedrock
- Bygg en dubbelmodell AWS Lambda med Python och Terraform
Externa resurser
- Dependency injection i Python – Real Python
- Hur dependency injection i Python förbättrar kodstruktur – Volito Digital
- Python Dependency Injection Tutorial – DataCamp
- Dependency Injector – Officiell dokumentation
- Injector – Lättviktigt DI-ramverk
- FastAPI-beroenden – Officiella dokumentationen
- SOLID-principer i Python – Software Patterns Lexicon
- Python-protokoll och strukturell subtypering – PEP 544