Dependency injection in Python: bouw modulaire en testbare code
Leer hoe je je code modulair, testbaar en onderhoudbaar maakt door verschillende Pythondependency-injectionframeworks te begrijpen en te implementeren.
Dependency injection is een ontwerppatroon waarbij objecten of services (we bedoelen hier dependencies) van buitenaf aan een class worden geleverd, in plaats van dat de class ze intern aanmaakt. Dit ontkoppelt componenten.
Dependency injection in Python gebruiken heeft veel voordelen, zoals:
Modulariteit: Je code wordt opgedeeld in kleinere, herbruikbare onderdelen.
Testbaarheid: Het is makkelijker om je code te testen omdat je het echte onderdeel kunt vervangen door mocks.
Onderhoudbaarheid: Je kunt delen van je code bijwerken of aanpassen zonder de rest van het systeem te breken.
In deze tutorial laat ik met eenvoudige, praktische voorbeelden zien hoe je dependency injection in Python gebruikt.
TL;DR
Dependency injection is een ontwerppatroon waarbij een class zijn dependencies van buitenaf ontvangt in plaats van ze zelf aan te maken. Het maakt je code modulairder.
Je kunt dependencies injecteren via de constructor, een settermethode of direct in een methode. Python ondersteunt dit handmatig of via frameworks zoals dependency-injector, injector en punq.
Populaire webframeworks gaan er verschillend mee om: FastAPI heeft ingebouwde ondersteuning via Depends(). Flask en Django hebben een library zoals dependency-injector nodig.
Dependency injection in Python begrijpen
Om te snappen hoe je dependency injection implementeert, is het belangrijk dat je de kernprincipes erachter begrijpt.
Dependency injection is gebaseerd op het principe Inversion of Control (IoC): in plaats van dat een class zijn dependencies aanmaakt en beheert, krijgt hij die van een externe bron. Dit helpt om je te focussen op één taak en houdt je code schoon.
Dependencies injecteren in Python kan op deze veelvoorkomende manieren, al zijn er ook andere:
Constructorinjectie: Geef dependencies mee via de classconstructor.
Setterinjectie: Stel dependencies in met een settermethode nadat het object is aangemaakt.
Methode-injectie: Geef dependencies direct mee aan de methode die ze nodig heeft.
Waarom dependency injection in Python gebruiken?
Als developer kan het begrijpen en toepassen van dependency injection veel verschil maken in je codeontwerp. Laten we naar een paar voordelen kijken.
Modulariteit, herbruikbaarheid en flexibiliteit
Dependency injection helpt je om je code op te delen in kleinere, meer gefocuste componenten, wat meer flexibiliteit en modulariteit geeft.
Elk deel of component handelt een specifieke taak af en vertrouwt op externe dependencies, waardoor hergebruik in verschillende delen van jouw of andermans applicatie eenvoudiger wordt
Stel dat je een class hebt die berichten verstuurt. In plaats van e-mail- of sms-logica direct in je service te bouwen, kun je een e-mail- of sms-senderclass injecteren.
Je class die berichten verstuurt maakt zich dan niet druk om hoe het bericht wordt verstuurd, maar gebruikt simpelweg de meegegeven sender. Dit betekent dat je je berichtenzender met verschillende e-mail- of sms-senders kunt gebruiken.
Onderhoudbaarheid en testbaarheid
Met dependency injection kun je tijdens het testen echte dependencies eenvoudig vervangen door mockdata. Zo schrijf je makkelijker unittests zonder afhankelijk te zijn van API’s of databases.
Stel je als gedachtenoefening voor dat je een class hebt die data in een database opslaat. Voor testdoeleinden kun je een mockdatabase injecteren in plaats van een echte, zodat je test niet aan de productiedatabase gekoppeld is.
Dependency injection maakt het mogelijk om specifieke delen van je code aan te passen zonder het hele systeem te breken. Dit is vooral handig als je in een applicatie van betaalprovider moet wisselen, zoals van Stripe naar PayPal, zonder de rest van je code aan te passen.
Inversion of Control en uitwisselbare dependencies
Het idee achter inversion of control is dit: dependency injection verschuift de verantwoordelijkheid voor het aanmaken en beheren van dependencies naar een externe bron.
Dit betekent dat je eenvoudig de ene implementatie kunt inwisselen voor een andere; bijvoorbeeld als je een ReportGenerator-class hebt die rapporten opmaakt en exporteert.
In plaats van dat de class beslist in welk formaat er geëxporteerd wordt, kun je er een exporterclass in injecteren, zoals PDFExporter, ExcelExporter of CSVExporter.
De ReportGenerator-class maakt zich niet druk om hoe de export gebeurt; om van formaat te wisselen, vervang je simpelweg de geïnjecteerde exporter, zonder de ReportGenerator zelf te wijzigen.
Dependency injection in Python implementeren
Nu je een duidelijk beeld hebt van wat dependency injection is en waarom het nuttig is, gaan we kijken hoe je het in Python implementeert. We beginnen met de eenvoudigste methode: handmatige dependency injection.
Handmatige dependency injection
Bij handmatige dependency injection maak je dependencies zelf aan en geef je ze door aan de class of functie die ze nodig heeft.
Deze methode werkt goed voor kleine applicaties, maar naarmate je applicatie groeit, heb je een dependency-injectionframework nodig om fouten te voorkomen.
Hier is een voorbeeld van code zonder dependency injection
classEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self):
self.email_service = EmailService()# Creates its own dependencydefnotify(self, message):
self.email_service.send_email(message)
notifier = UserNotifier()
notifier.notify("Welcome!")Mogelijk gemaakt door
In het bovenstaande voorbeeld is UserNotifier sterk gekoppeld aan EmailService. Je kunt EmailService niet eenvoudig vervangen of mocken voor tests.
Hier is een andere versie, maar met handmatige dependency injection.
classEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self, email_service):
self.email_service = email_service # Dependency is injecteddefnotify(self, message):
self.email_service.send_email(message)
email_service = EmailService()
notifier = UserNotifier(email_service)
notifier.notify("Welcome!")Mogelijk gemaakt door
De versie hierboven is flexibel en stelt je in staat om verschillende berichtendiensten te injecteren; je kunt EmailService ook vervangen door een mock in tests.
Het dependency-injector-framework gebruiken
Naarmate je applicatie groeit, wordt het complex en rommelig om dependencies te beheren.
Container bepaalt hoe instanties worden aangemaakt
Singleton zorgt ervoor dat er maar één instantie van EmailService bestaat
Factory maakt een nieuwe UserNotifier met de EmailService automatisch geïnjecteerd.
Wiringmechanisme
Je kunt decorators zoals @inject gebruiken om de noodzaak van handmatige dependencydoorgifte te verminderen.
Hier is een andere versie van het vorige voorbeeld:
from dependency_injector import containers, providers
# Define servicesclassEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self, email_service: EmailService):
self.email_service = email_service
defnotify(self, message):
self.email_service.send_email(message)# Create containerclassContainer(containers.DeclarativeContainer):
email_service = providers.Singleton(EmailService)
user_notifier = providers.Factory(UserNotifier, email_service=email_service)from dependency_injector.wiring import inject, Provide
@injectdefmain(notifier: UserNotifier = Provide[Container.user_notifier]):
notifier.notify("Hello!")if __name__ =="__main__":
container = Container()
container.wire(modules=[__name__])# This wires the current module
main()Mogelijk gemaakt door
In de huidige versie:
Zonder de dependency handmatig toe te voegen injecteert de @inject-decorator automatisch de functie- of classdependency.
De Provide-class vertelt dependency-injector welke dependency moet worden geïnjecteerd.
Deze aanpak haalt de noodzaak weg om dependencies handmatig te wringen vanuit de container.
Dit is handig in grotere applicaties waarin services van meerdere componenten kunnen afhangen (vandaar de behoefte om ze automatisch te initialiseren).
Dependency injection gebruiken in populaire Pythonframeworks
De meeste moderne Pythonframeworks maken het makkelijk om met dependency injection te werken, zodat je schonere en beter testbare code kunt schrijven.
In dit deel leer je hoe dependency injection wordt toegepast in Flask, Django en FastAPI.
Dependency injection in Flask
Flask heeft geen ingebouwde ondersteuning voor dependency injection, maar je kunt het implementeren met de dependency-injector-library via de volgende stappen.
Stap 1: Definieer je services
classGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Mogelijk gemaakt door
Stap 2: Richt je dependency-injectioncontainer in
from dependency_injector import containers, providers
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Mogelijk gemaakt door
Stap 3: Maak de Flaskapplicatie en injecteer dependencies.
from flask import Flask, request
from dependency_injector.wiring import inject, Provide
container = Container()
app = Flask(__name__)@app.route("/greet")@injectdefgreet(greeting_service: GreetingService = Provide[Container.greeting_service]):
name = request.args.get("name","World")return greeting_service.get_greeting(name)
container.wire(modules=[__name__])if __name__ =="__main__":
app.run(debug=True)Mogelijk gemaakt door
Uit de code hierboven:
De functie name = request.args.get(“name”, “World”) haalt de name uit de querystring en gebruikt standaard ”World” als die niet is meegegeven.
return greeting_service.get_greeting(name) roept de methode aan op de geïnjecteerde service en retourneert een begroetingsbericht.
container.wire(modules=[__name__]) vertelt de dependency injector om deze module te scannen op @inject-decorators en Provide[...]-markeringen om ze aan de daadwerkelijke providers te koppelen.
Stap 4: Test de applicatie
Voer de applicatie uit in je terminal met het commando python <name_of_file>.py en ga naar de URL http://localhost:5000/greet?name=Jacob.
Je zou het volgende moeten zien.
Dependency injection in Django
Net als Flask biedt Django geen ingebouwde dependency injection. Maar je kunt zijn class-based views, middleware en app-structuur gebruiken om dependency injection handmatig te integreren of met de dependency-injector-library.
Hier is een stapsgewijze procedure om dependency injection in Django te implementeren.
Stap 1: Definieer je service
Maak een eenvoudige service met je businesslogica in een nieuw bestand services.py in je appmap.
# myapp/services.pyclassGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Mogelijk gemaakt door
Stap 2: Richt de dependency-injectorcontainer in
Maak binnen je applicatiemap een container met de dependency-injector-library in een nieuw bestand containers.py.
# myapp/containers.pyfrom dependency_injector import containers, providers
from.services import GreetingService
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Mogelijk gemaakt door
Stap 3: Maak een view met geïnjecteerde dependencies
Gebruik de @inject-decorator in je view om dependencies te ontvangen in views.py.
# myapp/views.pyfrom django.http import HttpResponse
from dependency_injector.wiring import inject, Provide
from.containers import Container
from.services import GreetingService
@injectdefgreet_view(
request, greeting_service: GreetingService = Provide[Container.greeting_service]):
name = request.GET.get("name","World")
message = greeting_service.get_greeting(name)return HttpResponse(message)Mogelijk gemaakt door
Stap 4: Werk urls.py bij
Ga in je projectmap naar het bestand urls.py en koppel je dependency injector aan Django’s URL-routingsysteem.
from django.contrib import admin
from django.urls import path
from myapp.views import greet_view
urlpatterns =[
path("admin/", admin.site.urls),
path("greet/", greet_view),]Mogelijk gemaakt door
Stap 5: Wire de container in apps.py
Ga naar apps.py en wire de container aan de applicatie.
# myapp/apps.pyfrom django.apps import AppConfig
from.containers import Container
classMyAppConfig(AppConfig):
default_auto_field ="django.db.models.BigAutoField"
name ="myapp"defready(self):
container = Container()
container.wire(modules=["myapp.views"])Mogelijk gemaakt door
Ga vervolgens naar je appconfig in INSTALLED_APPS en voeg je applicatie toe.
Stap 3: Richt de FastAPI-app in en gebruik dependency injection
app = FastAPI()@app.get("/greet")defgreet(
name:str="World", service: GreetingService = Depends(get_greeting_service)):return{"message": service.get_greeting(name)}Mogelijk gemaakt door
Stap 4: Run en test de applicatie
Start je server met het commando uvicorn main:app –reload en bezoek de URL http://localhost:8000/greet?name=FastAPI. Je zou de volgende response moeten zien.
Python dependency-injectorframeworks vergelijken
Er zijn verschillende Pythondependencyframeworks, elk met eigen sterke punten, syntaxis en use-cases. De juiste keuze hangt af van de grootte en structuur van je project.
Hier is een vergelijking van enkele gangbare Pythondependency-injectionframeworks.
Biedt dependency injection voor views en middleware
Afhankelijk van injector
Geavanceerde implementatiepatronen
Naarmate de codebase van je applicatie groter wordt, wordt dependency injection lastiger te onderhouden.
Je kunt geavanceerde dependency injection gebruiken voor taken als request-scoped resources, cleanup en separation of concerns.
Scoped dependencies
Scoped dependencies stellen je in staat te bepalen hoelang een dependency blijft bestaan, bijvoorbeeld wanneer je een nieuw object per request wilt, een gedeeld object voor de levensduur van de app of automatische cleanup van resources zoals databaseverbindingen.
De meeste dependency-injectorframeworks ondersteunen scopes zoals:
Singleton: Eén instantie voor de hele app.
Factory: Een nieuwe instantie elke keer dat hij wordt geïnjecteerd.
Thread-local of request: Eén instantie per thread/request.
Resource: Beheerd met setup- en teardownlogica.
Hieronder een voorbeeld van scoped dependencies met geneste containers met dependency-injector.
from dependency_injector import containers, providers, resources
# A simple resource that needs setup and cleanupclassMyResource:def__enter__(self):print("Setting up the resource")return self
def__exit__(self, exc_type, exc_val, exc_tb):print("Cleaning up the resource")# Create a container to manage the resourceclassMyContainer(containers.DeclarativeContainer):
resource = providers.Resource(MyResource)# Using the resource with a context manager
container = MyContainer()with container.resource()as r:print("Using the resource")Mogelijk gemaakt door
In het voorbeeld hierboven bootst MyResource een class na die setup en teardown vereist, zoals het openen/sluiten van een bestand of database.
De container levert de resource met providers.Resource, en met with container.resource() wordt de context correct betreden en verlaten.
Dit patroon zorgt ervoor dat resources, zoals filehandles, databaseverbindingen of netwerksessies, netjes worden beheerd.
Stel je een API-endpoint voor dat een databasesessie nodig heeft voor het huidige request of een logger met requestspecifieke context.
Met scoped dependencies maakt het framework een request-scoped container aan wanneer het request start en worden alle dependencies uit die scope opgelost.
Wanneer het request eindigt, vindt eventuele noodzakelijke cleanup, zoals het sluiten van databasesessies, automatisch plaats. Dit zorgt ervoor dat state niet tussen requests gedeeld wordt en voorkomt resourcelekken.
Asynchrone injectie
Voor efficiënte afhandeling van I/O-gebonden taken heb je asynchrone uitvoering nodig. Dependency-injectionsystemen in frameworks zoals FastAPI en dependency-injector bieden asynchrone injectie met async/await.
Asynchrone injectie betekent het injecteren van dependencies die async def-functies zijn of het beheren van asynchrone resources zoals async databaseverbindingen, externe API’s of backgroundtasks.
In plaats van ze synchroon op te lossen, wacht het dependency-injectionsysteem tot ze zijn opgelost vóór injectie.
Hieronder een voorbeeld van een async dependency in FastAPI:
from fastapi import FastAPI, Depends
app = FastAPI()# Async dependencyasyncdefget_user():# Simulate async I/Oawait some_async_db_query()return{"name":"Philip Jones"}@app.get("/profile")asyncdefread_profile(user:dict= Depends(get_user)):return{"user": user}Mogelijk gemaakt door
FastAPI handelt de asynchrone afhandeling van get_user automatisch af en injecteert het resultaat.
dependency-injector ondersteunt ook async levenscyclusbeheer met AsyncResource-providers.
from dependency_injector import containers, providers, resources
classAsyncDB:asyncdefconnect(self):print("Connected")return self
asyncdefdisconnect(self):print("Disconnected")classAsyncDBResource(resources.AsyncResource):asyncdefinit(self)-> AsyncDB:
db = AsyncDB()returnawait db.connect()asyncdefshutdown(self, db: AsyncDB):await db.disconnect()classContainer(containers.DeclarativeContainer):
db = providers.Resource(AsyncDBResource)# Usageasyncdefmain():
container = Container()await container.init_resources()
db =await container.db()# Get the AsyncDB instance# Use db here...await container.shutdown_resources()Mogelijk gemaakt door
In de code hierboven:
resources.AsyncResource wordt gebruikt voor async setup-/teardownlogica.
await container.init_resources() initialiseert alle resources en roept init aan.
await container.shutdown_resources() ruimt alle resources op.
await container.db() haalt de daadwerkelijke resource-instantie op, namelijk AsyncDB.
Met asynchrone dependency injection houd je je applicatie performant door synchrone I/O in asynchrone routes te vermijden, en hoef je dependencies in je logica niet handmatig te awaiten.
Beveiligingsimplicaties
Hoewel dependency injection modulariteit en testbaarheid verbetert, kan het potentiële beveiligingsrisico’s introduceren als het niet zorgvuldig wordt beheerd.
Een belangrijk risico is dependency confusion, waarbij kwaadaardige pakketten legitieme nabootsen. Zo hebben aanvallers pakketten geüpload naar PyPI met namen die in interne bedrijfssystemen worden gebruikt om apps te misleiden de kwaadaardige versie te gebruiken.
Blijf veilig door nooit onbetrouwbare of door gebruikers aangestuurde componenten te injecteren en valideer configuratiebronnen, zoals omgevingsvariabelen, voordat je ze injecteert.
Gebruik requirements.txt- of poetry.lock-bestanden om exacte versies te vergrendelen, en automatiseer securityscans bij dependencywijzigingen. Zorg er daarnaast voor dat je je dependencies regelmatig op kwetsbaarheden controleert.
Het centraliseren van de controle over je dependencies is ook essentieel, omdat dit consistentie over je applicaties waarborgt en het toepassen van securitypatches of versie-updates makkelijker maakt. Het helpt om “shadow dependencies” te vermijden die op verschillende manieren door de codebase worden geïnjecteerd.
Verschillende tools kunnen risico’s detecteren en je securitypostuur verbeteren:
Snyk: Scant op bekende kwetsbaarheden in Pythonpakketten en stelt fixes voor.
Safety: Controleert op onveilige pakketten en codekwetsbaarheden.
Bandit: Een statische code-analyzer voor Python gericht op securityproblemen.
Toepassingen in de echte wereld
Dependency injection is niet alleen een theoretisch patroon; het wordt in allerlei productiesystemen gebruikt om complexiteit te beheren, flexibiliteit te verbeteren en testen te stroomlijnen.
Configuratie van webservices
In moderne webapplicaties speelt dependency injection een cruciale rol bij het beheren van standaardservices, zoals authenticatie, logging en database‑toegang.
Frameworks zoals FastAPI spelen een essentiële rol bij het oplossen van routes en dependencies.
Het onderstaande voorbeeld illustreert een gecentraliseerde authenticatie-implementatie in FastAPI.
from fastapi import Depends, HTTPException
defget_current_user(token:str= Depends(oauth2_scheme)):
user = verify_token(token)ifnot user:raise HTTPException(status_code=401)return user
@app.get("/protected")defprotected_route(user:dict= Depends(get_current_user)):return{"message":f"Hello {user['name']}"}Mogelijk gemaakt door
In de code hierboven wordt get_current_user() als routedependency geïnjecteerd, maar de logica voor het controleren van de token is gecentraliseerd en herbruikbaar.
Je kunt hetzelfde doen voor database‑toegang, zodat databasesessies automatisch worden beheerd en opgeschoond.
Hier is een voorbeeld van hoe je dependency injection kunt gebruiken om database‑toegang te centraliseren.
from sqlalchemy.ext.asyncio import AsyncSession
asyncdefget_db_session()-> AsyncSession:asyncwith async_session()as session:yield session
@app.get("/items")asyncdefread_items(db: AsyncSession = Depends(get_db_session)):returnawait db.execute("SELECT * FROM items")Mogelijk gemaakt door
In de code hierboven is get_db_session een dependencyprovider. Wanneer de route read_items wordt aangeroepen, injecteert FastAPI automatisch de parameter db, zodat de route toegang heeft tot de database zonder de sessie handmatig te beheren of op te ruimen.
Test-driven development
Dependency injection maakt het makkelijk om te veranderen welke dependencies componenten ontvangen, wat belangrijk is bij testen.
Je kunt echte services tijdens tests vervangen door mocks, waardoor je code modularer en eenvoudiger te testen wordt.
FastAPI en dependency-injector ondersteunen het overriden van dependencies tijdens tests.
Hier is een voorbeeld in FastAPI.
from fastapi.testclient import TestClient
from main import app, get_db_session
defoverride_db():return TestDBSession()
app.dependency_overrides[get_db_session]= override_db
client = TestClient(app)
response = client.get("/items")assert response.status_code ==200Mogelijk gemaakt door
In het codevoorbeeld hierboven wordt get_db_session tijdens tests overschreven met een mockdatabase; je hoeft de productiecode dus niet te patchen of aan te passen.
Ook in depedency-injector kun je je echte database overschrijven met een mockdatabase voor testdoeleinden, zoals in het voorbeeld hieronder.
from containers import Container
deftest_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()assert service.get_data()=="mocked"Mogelijk gemaakt door
De methode override() vervangt de echte dependency door een mock: schoon, gecontroleerd en omkeerbaar.
Hier zijn een paar strategieën die je kunt gebruiken voor dependency overrides:
Constructorinjectie: Zorg dat je mocks direct in de constructor meegeeft.
Frameworkoverrides: Gebruik ingebouwde override-tools zoals FastAPI.dependency_overrides in FastAPI of dependency_injector.override() in dependency-injector.
Fixtures: Gebruik testframeworks zoals pytest om herbruikbare mocks te maken en ze via fixtures te injecteren.
Best practices en antipatterns
Hoewel dependency injection is bedoeld om code modulair en eenvoudig testbaar te maken, kan het tot bugs leiden of complexiteit creëren.
Aanbevolen werkwijzen
Hier zijn wat best practices om je aan te houden bij het opzetten van dependencies:
Programmeur naar interfaces, niet naar implementaties: Definieer je dependencies met abstracte basisklassen of interfaces in plaats van complexe implementaties. Dit maakt je code makkelijker te testen en stimuleert het gebruik van mocks in unittests.
Geef de voorkeur aan compositie boven overerving: Stel objecten samen met geïnjecteerde dependencies in plaats van complexe classhiërarchieën te bouwen, om componenten klein en gefocust te houden.
Gecentraliseerde dependencyconfiguratie: Gebruik één container of module om dependencies te definiëren en te beheren, waardoor overriden en testen eenvoudiger wordt.
Vermijd circulaire dependencies: Ontwerp je dependencygraph zorgvuldig om cirkelverwijzingen te voorkomen, bijvoorbeeld door factoryfuncties of providers te gebruiken om instantiering uit te stellen.
Veelvoorkomende valkuilen
Vermijd de volgende valkuilen bij het gebruik van dependency injection.
Service locator‑antipatroon: Vermijd een globale container om dynamisch dependencies op te halen; dit kan dependencies verbergen en code moeilijk te begrijpen en te testen maken.
Over‑injectie: Injecteer niet te veel services in één class of functie om opgeblazen constructors en lastig te testen logica te voorkomen.
Scopemismanagement en weglekkende state: Gebruik altijd de juiste scope om te voorkomen dat state tussen gebruikers of tests gedeeld wordt.
Dependency injection testen in Python
Dependency injection maakt het eenvoudig om mockobjecten in je componenten te injecteren en vereenvoudigt zo unittests. Dit laat je fundamentele componenten scheiden van testspecifieke.
Met dependency injection zijn tests deterministisch, hebben ze geen bijwerkingen en voeren ze sneller uit.
Hier is een voorbeeld dat laat zien hoe je dependency injection kunt gebruiken om mock-, stub- of fakeversies van die dependencies in je tests te injecteren.
classEmailService:defsend_email(self, to:str, message:str):...classNotifier:def__init__(self, email_service: EmailService):
self.email_service = email_service
defalert(self, user_email):
self.email_service.send_email(user_email,"Alert!")Mogelijk gemaakt door
Vervang in tests EmailService door een mock:
classTestEmailService:defsend_email(self, to, message):
self.sent_to = to
deftest_notifier_sends_email():
test_email = TestEmailService()
notifier = Notifier(test_email)
notifier.alert("[email protected]")assert test_email.sent_to =="[email protected]"Mogelijk gemaakt door
Gebruik dependency injection zo vaak mogelijk en vermijd monkeypatching voor onderhoudbaarheid en om brekende wijzigingen te voorkomen wanneer interne namen veranderen.
Conclusie
Hoewel je voor kleine projecten misschien niet direct dependency injection nodig hebt, profiteer je naarmate het project groeit van de structuur en modulariteit die dependency injection biedt.
Vooruitkijkend is de toekomst van dependency injection in Python veelbelovend, met een verbeterd typesysteem en betere async lifecycle‑management.
Dependency injection beheersen is essentieel om een vaardige Pythonontwikkelaar te worden. Maar daar stopt het niet; er zijn nog veel andere geavanceerde concepten die je Pythonskills kunnen verhogen.
Om je leercurve te versnellen, zijn hier wat resources die je kunt bekijken.
Ervaren docent data science en biostatisticus met expertise in Python, R en machine learning.
FAQs
Wat is dependency injection?⌄
Dependency injection is een ontwerppatroon dat je helpt schonere code te schrijven door dependencies binnen een class van buitenaf te laten aanreiken, waardoor je code makkelijker te beheren en aan te passen is.
Wat zijn de belangrijkste voordelen van dependency injection?⌄
Dependency injection maakt je code modulair, testbaar en onderhoudbaar.
Hoe kan ik dependency injection in Python implementeren?⌄
Je kunt dependency injection in Python handmatig implementeren of met libraries zoals dependency_injector.
Wat is asynchrone injectie?⌄
Asynchrone injectie betekent het injecteren van dependencies die async def-functies zijn of het beheren van async resources zoals async databaseverbindingen, externe API’s of backgroundtasks.
Wat zijn de beveiligingsrisico’s van dependency injection en hoe ga ik daarmee om?⌄
Een belangrijk risico is dependency confusion, waarbij kwaadaardige pakketten legitieme nabootsen. Ter voorkoming kun je tools zoals Snyk, Xygeni, Safety of Bandit gebruiken om je code en pakketten op kwetsbaarheden te scannen.
Dependency injection is een ontwerppatroon waarbij objecten of services (we bedoelen hier dependencies) van buitenaf aan een class worden geleverd, in plaats van dat de class ze intern aanmaakt. Dit ontkoppelt componenten.
Dependency injection in Python gebruiken heeft veel voordelen, zoals:
In deze tutorial laat ik met eenvoudige, praktische voorbeelden zien hoe je dependency injection in Python gebruikt.
TL;DR
Dependency injection is een ontwerppatroon waarbij een class zijn dependencies van buitenaf ontvangt in plaats van ze zelf aan te maken. Het maakt je code modulairder.
Je kunt dependencies injecteren via de constructor, een settermethode of direct in een methode. Python ondersteunt dit handmatig of via frameworks zoals
dependency-injector,injectorenpunq.Populaire webframeworks gaan er verschillend mee om: FastAPI heeft ingebouwde ondersteuning via
Depends(). Flask en Django hebben een library zoalsdependency-injectornodig.Dependency injection in Python begrijpen
Om te snappen hoe je dependency injection implementeert, is het belangrijk dat je de kernprincipes erachter begrijpt.
Dependency injection is gebaseerd op het principe Inversion of Control (IoC): in plaats van dat een class zijn dependencies aanmaakt en beheert, krijgt hij die van een externe bron. Dit helpt om je te focussen op één taak en houdt je code schoon.
Dependencies injecteren in Python kan op deze veelvoorkomende manieren, al zijn er ook andere:
Waarom dependency injection in Python gebruiken?
Als developer kan het begrijpen en toepassen van dependency injection veel verschil maken in je codeontwerp. Laten we naar een paar voordelen kijken.
Modulariteit, herbruikbaarheid en flexibiliteit
Dependency injection helpt je om je code op te delen in kleinere, meer gefocuste componenten, wat meer flexibiliteit en modulariteit geeft.
Elk deel of component handelt een specifieke taak af en vertrouwt op externe dependencies, waardoor hergebruik in verschillende delen van jouw of andermans applicatie eenvoudiger wordt
Stel dat je een class hebt die berichten verstuurt. In plaats van e-mail- of sms-logica direct in je service te bouwen, kun je een e-mail- of sms-senderclass injecteren.
Je class die berichten verstuurt maakt zich dan niet druk om hoe het bericht wordt verstuurd, maar gebruikt simpelweg de meegegeven sender. Dit betekent dat je je berichtenzender met verschillende e-mail- of sms-senders kunt gebruiken.
Onderhoudbaarheid en testbaarheid
Met dependency injection kun je tijdens het testen echte dependencies eenvoudig vervangen door mockdata. Zo schrijf je makkelijker unittests zonder afhankelijk te zijn van API’s of databases.
Stel je als gedachtenoefening voor dat je een class hebt die data in een database opslaat. Voor testdoeleinden kun je een mockdatabase injecteren in plaats van een echte, zodat je test niet aan de productiedatabase gekoppeld is.
Dependency injection maakt het mogelijk om specifieke delen van je code aan te passen zonder het hele systeem te breken. Dit is vooral handig als je in een applicatie van betaalprovider moet wisselen, zoals van Stripe naar PayPal, zonder de rest van je code aan te passen.
Inversion of Control en uitwisselbare dependencies
Het idee achter inversion of control is dit: dependency injection verschuift de verantwoordelijkheid voor het aanmaken en beheren van dependencies naar een externe bron.
Dit betekent dat je eenvoudig de ene implementatie kunt inwisselen voor een andere; bijvoorbeeld als je een
ReportGenerator-class hebt die rapporten opmaakt en exporteert.In plaats van dat de class beslist in welk formaat er geëxporteerd wordt, kun je er een exporterclass in injecteren, zoals
PDFExporter,ExcelExporterofCSVExporter.De
ReportGenerator-class maakt zich niet druk om hoe de export gebeurt; om van formaat te wisselen, vervang je simpelweg de geïnjecteerde exporter, zonder deReportGeneratorzelf te wijzigen.Dependency injection in Python implementeren
Nu je een duidelijk beeld hebt van wat dependency injection is en waarom het nuttig is, gaan we kijken hoe je het in Python implementeert. We beginnen met de eenvoudigste methode: handmatige dependency injection.
Handmatige dependency injection
Bij handmatige dependency injection maak je dependencies zelf aan en geef je ze door aan de class of functie die ze nodig heeft.
Deze methode werkt goed voor kleine applicaties, maar naarmate je applicatie groeit, heb je een dependency-injectionframework nodig om fouten te voorkomen.
Hier is een voorbeeld van code zonder dependency injection
In het bovenstaande voorbeeld is
UserNotifiersterk gekoppeld aanEmailService. Je kuntEmailServiceniet eenvoudig vervangen of mocken voor tests.Hier is een andere versie, maar met handmatige dependency injection.
De versie hierboven is flexibel en stelt je in staat om verschillende berichtendiensten te injecteren; je kunt
EmailServiceook vervangen door een mock in tests.Het dependency-injector-framework gebruiken
Naarmate je applicatie groeit, wordt het complex en rommelig om dependencies te beheren.
Het
dependency-injectorframework biedt een gestructureerde aanpak.Container-providerarchitectuur
Het
dependency-injector-framework werkt op basis van een container-providerarchitectuur.In het bovenstaande voorbeeld:
Containerbepaalt hoe instanties worden aangemaaktSingletonzorgt ervoor dat er maar één instantie vanEmailServicebestaatFactorymaakt een nieuweUserNotifiermet deEmailServiceautomatisch geïnjecteerd.Wiringmechanisme
Je kunt decorators zoals
@injectgebruiken om de noodzaak van handmatige dependencydoorgifte te verminderen.Hier is een andere versie van het vorige voorbeeld:
In de huidige versie:
Zonder de dependency handmatig toe te voegen injecteert de
@inject-decorator automatisch de functie- of classdependency.De
Provide-class verteltdependency-injectorwelke dependency moet worden geïnjecteerd.Deze aanpak haalt de noodzaak weg om dependencies handmatig te wringen vanuit de container.
Dit is handig in grotere applicaties waarin services van meerdere componenten kunnen afhangen (vandaar de behoefte om ze automatisch te initialiseren).
Dependency injection gebruiken in populaire Pythonframeworks
De meeste moderne Pythonframeworks maken het makkelijk om met dependency injection te werken, zodat je schonere en beter testbare code kunt schrijven.
In dit deel leer je hoe dependency injection wordt toegepast in Flask, Django en FastAPI.
Dependency injection in Flask
Flask heeft geen ingebouwde ondersteuning voor dependency injection, maar je kunt het implementeren met de
dependency-injector-library via de volgende stappen.Stap 1: Definieer je services
Stap 2: Richt je dependency-injectioncontainer in
Stap 3: Maak de Flaskapplicatie en injecteer dependencies.
Uit de code hierboven:
De functie
name = request.args.get(“name”, “World”)haalt denameuit de querystring en gebruikt standaard”World”als die niet is meegegeven.return greeting_service.get_greeting(name)roept de methode aan op de geïnjecteerde service en retourneert een begroetingsbericht.container.wire(modules=[__name__])vertelt de dependency injector om deze module te scannen op@inject-decorators enProvide[...]-markeringen om ze aan de daadwerkelijke providers te koppelen.Stap 4: Test de applicatie
Voer de applicatie uit in je terminal met het commando
python <name_of_file>.pyen ga naar de URLhttp://localhost:5000/greet?name=Jacob.Je zou het volgende moeten zien.
Dependency injection in Django
Net als Flask biedt Django geen ingebouwde dependency injection. Maar je kunt zijn class-based views, middleware en app-structuur gebruiken om dependency injection handmatig te integreren of met de
dependency-injector-library.Hier is een stapsgewijze procedure om dependency injection in Django te implementeren.
Stap 1: Definieer je service
Maak een eenvoudige service met je businesslogica in een nieuw bestand
services.pyin je appmap.Stap 2: Richt de dependency-injectorcontainer in
Maak binnen je applicatiemap een container met de
dependency-injector-library in een nieuw bestandcontainers.py.Stap 3: Maak een view met geïnjecteerde dependencies
Gebruik de
@inject-decorator in je view om dependencies te ontvangen inviews.py.Stap 4: Werk urls.py bij
Ga in je projectmap naar het bestand
urls.pyen koppel je dependency injector aan Django’s URL-routingsysteem.Stap 5: Wire de container in apps.py
Ga naar
apps.pyen wire de container aan de applicatie.Ga vervolgens naar je appconfig in
INSTALLED_APPSen voeg je applicatie toe.Stap 6: Test de view
Start je Djangoserver en open de view via de URL
http://localhost:8000/greet/?name=Django.Je zou het volgende moeten zien.
Dependency injection in FastAPI
FastAPI heeft ingebouwde ondersteuning voor dependency injection via de speciale marker
Depends()die injecteert wat een functie retourneert.Hier is een stapsgewijze gids voor dependency injection in FastAPI.
Stap 1: Definieer een service
Stap 2: Maak de dependencyfunctie
Stap 3: Richt de FastAPI-app in en gebruik dependency injection
Stap 4: Run en test de applicatie
Start je server met het commando
uvicorn main:app –reloaden bezoek de URLhttp://localhost:8000/greet?name=FastAPI. Je zou de volgende response moeten zien.Python dependency-injectorframeworks vergelijken
Er zijn verschillende Pythondependencyframeworks, elk met eigen sterke punten, syntaxis en use-cases. De juiste keuze hangt af van de grootte en structuur van je project.
Hier is een vergelijking van enkele gangbare Pythondependency-injectionframeworks.
Framework
Het beste voor
Sterke punten
Beperkingen
dependency-injectorComplexe applicaties in productie
Volledig uitgerust, snel en configureerbaar
Langdradig
injectorEenvoudig en geschikt voor minimale applicaties
Schone syntaxis
Minder geavanceerd dan anderen.
pinjectGeavanceerde bindinglogica
Auto-wiring
Langzamer door introspectie
punqEenvoudige applicaties
Minimaal en snel
Beperkte features vergeleken met anderen
FastAPI
Depends()FastAPI-applicaties
Ingebouwde ondersteuning voor async en ook testbaar
Niet herbruikbaar buiten FastAPI
Flask-injectorFlaskapplicaties
Eenvoudige integratie met Flaskapplicaties
Afhankelijk van
injectordjango-injectorDjangoprojecten
Biedt dependency injection voor views en middleware
Afhankelijk van
injectorGeavanceerde implementatiepatronen
Naarmate de codebase van je applicatie groter wordt, wordt dependency injection lastiger te onderhouden.
Je kunt geavanceerde dependency injection gebruiken voor taken als request-scoped resources, cleanup en separation of concerns.
Scoped dependencies
Scoped dependencies stellen je in staat te bepalen hoelang een dependency blijft bestaan, bijvoorbeeld wanneer je een nieuw object per request wilt, een gedeeld object voor de levensduur van de app of automatische cleanup van resources zoals databaseverbindingen.
De meeste dependency-injectorframeworks ondersteunen scopes zoals:
Hieronder een voorbeeld van scoped dependencies met geneste containers met
dependency-injector.In het voorbeeld hierboven bootst
MyResourceeen class na die setup en teardown vereist, zoals het openen/sluiten van een bestand of database.De
containerlevert de resource metproviders.Resource, en metwith container.resource()wordt de context correct betreden en verlaten.Dit patroon zorgt ervoor dat resources, zoals filehandles, databaseverbindingen of netwerksessies, netjes worden beheerd.
Stel je een API-endpoint voor dat een databasesessie nodig heeft voor het huidige request of een logger met requestspecifieke context.
Met scoped dependencies maakt het framework een request-scoped container aan wanneer het request start en worden alle dependencies uit die scope opgelost.
Wanneer het request eindigt, vindt eventuele noodzakelijke cleanup, zoals het sluiten van databasesessies, automatisch plaats. Dit zorgt ervoor dat state niet tussen requests gedeeld wordt en voorkomt resourcelekken.
Asynchrone injectie
Voor efficiënte afhandeling van I/O-gebonden taken heb je asynchrone uitvoering nodig. Dependency-injectionsystemen in frameworks zoals FastAPI en
dependency-injectorbieden asynchrone injectie metasync/await.Asynchrone injectie betekent het injecteren van dependencies die
async def-functies zijn of het beheren vanasynchrone resourceszoals async databaseverbindingen, externe API’s of backgroundtasks.In plaats van ze synchroon op te lossen, wacht het dependency-injectionsysteem tot ze zijn opgelost vóór injectie.
Hieronder een voorbeeld van een async dependency in FastAPI:
FastAPI handelt de asynchrone afhandeling van
get_userautomatisch af en injecteert het resultaat.dependency-injectorondersteunt ook async levenscyclusbeheer metAsyncResource-providers.In de code hierboven:
resources.AsyncResourcewordt gebruikt voor async setup-/teardownlogica.await container.init_resources()initialiseert alle resources en roeptinitaan.await container.shutdown_resources()ruimt alle resources op.await container.db()haalt de daadwerkelijke resource-instantie op, namelijkAsyncDB.Met asynchrone dependency injection houd je je applicatie performant door synchrone I/O in asynchrone routes te vermijden, en hoef je dependencies in je logica niet handmatig te awaiten.
Beveiligingsimplicaties
Hoewel dependency injection modulariteit en testbaarheid verbetert, kan het potentiële beveiligingsrisico’s introduceren als het niet zorgvuldig wordt beheerd.
Een belangrijk risico is dependency confusion, waarbij kwaadaardige pakketten legitieme nabootsen. Zo hebben aanvallers pakketten geüpload naar PyPI met namen die in interne bedrijfssystemen worden gebruikt om apps te misleiden de kwaadaardige versie te gebruiken.
Blijf veilig door nooit onbetrouwbare of door gebruikers aangestuurde componenten te injecteren en valideer configuratiebronnen, zoals omgevingsvariabelen, voordat je ze injecteert.
Gebruik
requirements.txt- ofpoetry.lock-bestanden om exacte versies te vergrendelen, en automatiseer securityscans bij dependencywijzigingen. Zorg er daarnaast voor dat je je dependencies regelmatig op kwetsbaarheden controleert.Het centraliseren van de controle over je dependencies is ook essentieel, omdat dit consistentie over je applicaties waarborgt en het toepassen van securitypatches of versie-updates makkelijker maakt. Het helpt om “shadow dependencies” te vermijden die op verschillende manieren door de codebase worden geïnjecteerd.
Verschillende tools kunnen risico’s detecteren en je securitypostuur verbeteren:
Toepassingen in de echte wereld
Dependency injection is niet alleen een theoretisch patroon; het wordt in allerlei productiesystemen gebruikt om complexiteit te beheren, flexibiliteit te verbeteren en testen te stroomlijnen.
Configuratie van webservices
In moderne webapplicaties speelt dependency injection een cruciale rol bij het beheren van standaardservices, zoals authenticatie, logging en database‑toegang.
Frameworks zoals FastAPI spelen een essentiële rol bij het oplossen van routes en dependencies.
Het onderstaande voorbeeld illustreert een gecentraliseerde authenticatie-implementatie in FastAPI.
In de code hierboven wordt
get_current_user()als routedependency geïnjecteerd, maar de logica voor het controleren van de token is gecentraliseerd en herbruikbaar.Je kunt hetzelfde doen voor database‑toegang, zodat databasesessies automatisch worden beheerd en opgeschoond.
Hier is een voorbeeld van hoe je dependency injection kunt gebruiken om database‑toegang te centraliseren.
In de code hierboven is
get_db_sessioneen dependencyprovider. Wanneer de routeread_itemswordt aangeroepen, injecteert FastAPI automatisch de parameterdb, zodat de route toegang heeft tot de database zonder de sessie handmatig te beheren of op te ruimen.Test-driven development
Dependency injection maakt het makkelijk om te veranderen welke dependencies componenten ontvangen, wat belangrijk is bij testen.
Je kunt echte services tijdens tests vervangen door mocks, waardoor je code modularer en eenvoudiger te testen wordt.
FastAPI en
dependency-injectorondersteunen het overriden van dependencies tijdens tests.Hier is een voorbeeld in FastAPI.
In het codevoorbeeld hierboven wordt
get_db_sessiontijdens tests overschreven met een mockdatabase; je hoeft de productiecode dus niet te patchen of aan te passen.Ook in
depedency-injectorkun je je echte database overschrijven met een mockdatabase voor testdoeleinden, zoals in het voorbeeld hieronder.De methode
override()vervangt de echte dependency door een mock: schoon, gecontroleerd en omkeerbaar.Hier zijn een paar strategieën die je kunt gebruiken voor dependency overrides:
FastAPI.dependency_overridesin FastAPI ofdependency_injector.override()independency-injector.pytestom herbruikbare mocks te maken en ze via fixtures te injecteren.Best practices en antipatterns
Hoewel dependency injection is bedoeld om code modulair en eenvoudig testbaar te maken, kan het tot bugs leiden of complexiteit creëren.
Aanbevolen werkwijzen
Hier zijn wat best practices om je aan te houden bij het opzetten van dependencies:
Veelvoorkomende valkuilen
Vermijd de volgende valkuilen bij het gebruik van dependency injection.
Dependency injection testen in Python
Dependency injection maakt het eenvoudig om mockobjecten in je componenten te injecteren en vereenvoudigt zo unittests. Dit laat je fundamentele componenten scheiden van testspecifieke.
Met dependency injection zijn tests deterministisch, hebben ze geen bijwerkingen en voeren ze sneller uit.
Hier is een voorbeeld dat laat zien hoe je dependency injection kunt gebruiken om mock-, stub- of fakeversies van die dependencies in je tests te injecteren.
Vervang in tests
EmailServicedoor een mock:Gebruik dependency injection zo vaak mogelijk en vermijd monkeypatching voor onderhoudbaarheid en om brekende wijzigingen te voorkomen wanneer interne namen veranderen.
Conclusie
Hoewel je voor kleine projecten misschien niet direct dependency injection nodig hebt, profiteer je naarmate het project groeit van de structuur en modulariteit die dependency injection biedt.
Vooruitkijkend is de toekomst van dependency injection in Python veelbelovend, met een verbeterd typesysteem en betere async lifecycle‑management.
Dependency injection beheersen is essentieel om een vaardige Pythonontwikkelaar te worden. Maar daar stopt het niet; er zijn nog veel andere geavanceerde concepten die je Pythonskills kunnen verhogen.
Om je leercurve te versnellen, zijn hier wat resources die je kunt bekijken.
Cursussen:
Blogposts: