FastAPIs Strom mit Dependency Injection freischalten
Min-jun Kim
Dev Intern · Leapcell

Einleitung
Die Erstellung robuster und wartbarer APIs ist ein Eckpfeiler der modernen Softwareentwicklung. Wenn Anwendungen komplexer werden, kann die Verwaltung von Abhängigkeiten – den Objekten oder Diensten, die eine Klasse oder Funktion zur Ausführung ihrer Aufgabe benötigt – zu einer erheblichen Herausforderung werden. Traditionelle Ansätze führen oft zu eng gekoppeltem Code, was das Testen, Wiederverwenden und Weiterentwickeln erschwert. Hier glänzt Dependency Injection (DI) und bietet ein leistungsstarkes Muster zur Entkopplung von Komponenten und zur Verbesserung der Codeflexibilität. FastAPI, ein modernes, schnelles (leistungsstarkes) Web-Framework zum Erstellen von APIs mit Python 3.7+ auf Basis von Standard-Python-Typ-Hints, nutzt Dependency Injection als Kernfunktion. Dieser Artikel wird sich mit FastAPIs Dependency Injection System befassen, seine zugrundeliegenden Prinzipien entschlüsseln, seine vielfältigen Anwendungen aufzeigen und praktische Tipps für effektive Tests geben.
Kernkonzepte der Dependency Injection
Bevor wir uns mit den FastAPI-Spezifika befassen, sollten wir ein gemeinsames Verständnis der Schlüsselbegriffe im Zusammenhang mit Dependency Injection herstellen:
- Abhängigkeit (Dependency): Ein Objekt oder ein Dienst, das ein anderes Objekt (das abhängige Objekt) benötigt, um korrekt zu funktionieren. Beispielsweise ist ein Datenbankclient eine Abhängigkeit für einen Dienst, der mit der Datenbank interagiert.
- Abhängiges Objekt (Dependent): Das Objekt oder die Funktion, die eine oder mehrere Abhängigkeiten benötigt.
- Inversion of Control (IoC): Ein Designprinzip, bei dem der Kontrollfluss eines Systems gegenüber der traditionellen prozeduralen Programmierung umgekehrt wird. Anstatt dass das abhängige Objekt seine Abhängigkeiten direkt erstellt oder sucht, ist eine externe Entität (der Injector) dafür verantwortlich, diese bereitzustellen. Dependency Injection ist eine spezifische Technik zur Erreichung von IoC.
- Injector (oder DI-Container): Die Komponente, die für die Erstellung und Bereitstellung von Abhängigkeiten für abhängige Objekte verantwortlich ist. In FastAPI fungiert das Framework selbst als Injector.
- Provider (oder Abhängigkeitsfunktion): Eine Funktion oder Klasse, die FastAPI aufruft, um eine Abhängigkeit zu „bereitstellen“. Diese Funktionen sind oft mit
@Depends
dekoriert oder werden direkt in der Signatur der Routenfunktion referenziert.
FastAPIs Dependency Injection System erklärt
FastAPIs Dependency Injection System basiert auf den Standard-Python-Typ-Hints und dem Depends
-Dienstprogramm. Im Kern nutzt es die Introspektion von Funktionsargumenten in Python, um erforderliche Abhängigkeiten zu identifizieren.
Funktionsweise:
Wenn eine Anfrage eingeht, inspiziert FastAPI die Signatur der Pfadoperationsfunktion (die Funktion, die mit @app.get
, @app.post
usw. dekoriert ist). Für jeden Parameter in der Signatur der Pfadoperationsfunktion:
- Typ-Hint-Prüfung: Wenn ein Parameter einen Typ-Hint, aber keinen Standardwert hat, versucht FastAPI, ihn als Pfadparameter, Abfrageparameter oder Anforderungstext aufzulösen.
Depends
-Dienstprogramm: Wenn der Standardwert eines Parameters ein mitDepends()
umschlossenes Objekt ist, erkennt FastAPI es als Abhängigkeit. Das anDepends()
übergebene Argument ist normalerweise eine Abhängigkeitsfunktion oder eine Klasse.- Auflösung von Abhängigkeiten: FastAPI ruft dann die Abhängigkeitsfunktion auf (oder instanziiert die Klasse). Der Rückgabewert der Abhängigkeitsfunktion (oder das instanziierte Objekt) wird zum Wert für diesen Parameter in der Pfadoperationsfunktion.
- Verkettung von Abhängigkeiten: Abhängigkeitsfunktionen können ihrerseits eigene Abhängigkeiten deklarieren, wodurch eine Abfolge von Abhängigkeitsauflösungen entsteht. FastAPI handhabt dies rekursiv.
- Lebenszyklusverwaltung: FastAPI bietet Mechanismen zur Handhabung von Abhängigkeiten, die nach einer Anfrage aufgeräumt werden müssen, wie z. B. Datenbanksitzungen oder Dateihandles, indem
yield
in Abhängigkeitsfunktionen verwendet wird.
Lassen Sie uns dies anhand eines einfachen Beispiels veranschaulichen:
from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() # Eine einfache Abhängigkeitsfunktion, die die aktuelle Benutzerabfrage simuliert def get_current_user(token: str): if token == "secret-token": return {"username": "admin", "id": 1} raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Ungültige Authentifizierungsanmeldeinformationen", ) @app.get("/users/me/") async def read_current_user(current_user: dict = Depends(get_current_user)): return current_user # Um dieses Beispiel auszuführen: # uvicorn your_module_name:app --reload # Greifen Sie dann über Ihren Browser oder cURL auf http://127.0.0.1:8000/users/me/?token=secret-token zu. # Versuchen Sie es mit einem ungültigen Token: http://127.0.0.1:8000/users/me/?token=wrong-token
In diesem Beispiel:
get_current_user
ist unsere Abhängigkeitsfunktion. Sie erwartet eintoken
(das FastAPI standardmäßig aus Abfrageparametern abrufen wird).read_current_user
ist die Pfadoperationsfunktion. Sie deklariertcurrent_user
als Parameter mit dem StandardwertDepends(get_current_user)
.- Wenn
GET /users/me/
aufgerufen wird, ruft FastAPI zuerstget_current_user
auf. Wennget_current_user
einen Benutzer zurückgibt, wird dieses Benutzerobjekt dann als Argumentcurrent_user
anread_current_user
übergeben. Wennget_current_user
eineHTTPException
auslöst, wird diese Ausnahme von FastAPI behandelt.
Gängige Anwendungsfälle und Anwendungen
FastAPIs DI-System ist vielseitig und kann in zahlreichen Szenarien angewendet werden:
-
Datenbanksitzungsverwaltung: Bereitstellung einer Datenbanksitzungs- oder Verbindung für alle Pfadoperationsfunktionen, die mit einer Datenbank interagieren müssen. Dies gewährleistet eine ordnungsgemäße Sitzungsbehandlung (Öffnen, Committing/Rollback, Schließen).
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from fastapi import Depends, FastAPI SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close() app = FastAPI() @app.get("/items/") def read_items(db: Session = Depends(get_db)): # Verwenden Sie db für Datenbankoperationen # Zum Beispiel: return db.query(models.Item).all() return {"message": "Datenbanksitzung bereitgestellt"}
Hier verwendet
get_db
yield
, um den Lebenszyklus der Datenbanksitzung zu verwalten. Die Sitzung wird geöffnet, bevor die Routenfunktion ausgeführt wird, und danach geschlossen, auch wenn Fehler auftreten. -
Authentifizierung und Autorisierung: Wie im Beispiel
get_current_user
gezeigt, ist DI perfekt geeignet, um Benutzeranmeldeinformationen zu extrahieren, sie zu validieren und das authentifizierte Benutzerobjekt in Pfadoperationsfunktionen einzufügen. -
Einspeisen von Konfigurationseinstellungen: Bereitstellung anwendungsweiter Konfigurationsparameter (z. B. API-Schlüssel, Umgebungsvariablen) für relevante Teile der Anwendung.
from pydantic_settings import BaseSettings, SettingsConfigDict from fastapi import Depends, FastAPI from functools import lru_cache class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") app_name: str = "Meine fantastische App" admin_email: str = "admin@example.com" items_per_page: int = 10 @lru_cache() # Cache das Settings-Objekt für die Leistung def get_settings(): return Settings() app = FastAPI() @app.get("/info/") def get_app_info(settings: Settings = Depends(get_settings)): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_page": settings.items_per_page, }
Die Verwendung von
pydantic-settings
mitDepends
macht das Konfigurationsmanagement übersichtlich und testbar.@lru_cache
ist eine gute Optimierung für Abhängigkeiten, deren Erstellung aufwendig ist und die sich nicht pro Anfrage ändern. -
Einspeisen von Geschäftslogik/Diensten: Entkopplung der Geschäftslogik von den Routenhandlern durch Einspeisen von Service-Klassen oder -Funktionen.
from fastapi import FastAPI, Depends class ItemService: def get_all_items(self): return [{"id": 1, "name": "Artikel A"}, {"id": 2, "name": "Artikel B"}] def create_item(self, name: str): # Simulieren des Speicherns in der DB return {"id": 3, "name": name, "status": "erstellt"} def get_item_service(): return ItemService() app = FastAPI() @app.get("/items_service/") def list_items(item_service: ItemService = Depends(get_item_service)): return item_service.get_all_items() @app.post("/items_service/") def add_item(name: str, item_service: ItemService = Depends(get_item_service)): return item_service.create_item(name)
Hier kapselt
ItemService
die artikelbezogene Geschäftslogik und macht die Routenhandler übersichtlicher und einfacher unabhängig zu testen.
Überschreiben von Abhängigkeiten für Tests
Eines der leistungsfähigsten Features von FastAPIs DI-System, insbesondere für Entwicklung und Tests, ist die Möglichkeit, Abhängigkeiten zu überschreiben. Dies ermöglicht es Ihnen, echte Abhängigkeiten (wie eine Datenbankverbindung) während des Tests durch Mock-Objekte oder vereinfachte Versionen zu ersetzen, um sicherzustellen, dass Ihre Tests schnell, isoliert und zuverlässig sind.
Der Kontextmanager app.dependency_overrides
ist der primäre Mechanismus dafür:
from fastapi.testclient import TestClient from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() # Ursprüngliche Abhängigkeit def get_current_user_prod(token: str): if token == "prod-secret": return {"username": "prod_user", "id": 1} raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) @app.get("/protected/") def protected_route(user: dict = Depends(get_current_user_prod)): return {"message": f"Hallo, {user['username']}!"} # --- Test-Setup --- # Mock-Abhängigkeit für Tests def get_current_user_mock(): return {"username": "test_user", "id": 99} client = TestClient(app) def test_protected_route_with_mock_user(): # Überschreiben Sie get_current_user_prod mit get_current_user_mock app.dependency_overrides[get_current_user_prod] = get_current_user_mock response = client.get("/protected/") assert response.status_code == 200 assert response.json() == {"message": "Hallo, test_user!"} # Aufräumen der Überschreibung # Dies ist entscheidend, insbesondere in Testabläufge, in denen mehrere Tests ausgeführt werden # und die Überschreibung nachfolgende Tests beeinträchtigen könnte. app.dependency_overrides = {} # Beispiel für einen Test, der immer noch die ursprüngliche verwendet, wenn nicht überschrieben def test_protected_route_prod_user(): app.dependency_overrides = {} # Stellen Sie sicher, dass keine verbleibenden Überschreibungen vorhanden sind response = client.get("/protected/", headers={"Authorization": "Bearer prod-secret"}) # FastAPI parst 'token' hier nicht automatisch aus Headern zur Vereinfachung, gehen wir der Konsistenz halber mit früheren Beispielen von GET-Parametern aus assert response.status_code == 401 # Sollte fehlschlagen, wenn kein 'token'-Abfrageparameter vorhanden ist, wie ursprünglich. response = client.get("/protected/", params={"token":"prod-secret"}) assert response.status_code == 200 assert response.json() == {"message": "Hallo, prod_user!"}
Im Testbeispiel:
- Wir definieren
get_current_user_prod
als die „echte“ Abhängigkeit. - Wir definieren
get_current_user_mock
als eine vereinfachte Version für Tests, die nicht von externen Faktoren oder spezifischen Token-Werten abhängt. - Innerhalb von
test_protected_route_with_mock_user
ersetzen wirget_current_user_prod
vorübergehend durchget_current_user_mock
inapp.dependency_overrides
. Wenn nunclient.get("/protected/")
aufgerufen wird, verwendet FastAPIget_current_user_mock
anstelle. - Entscheidend ist, dass nach dem Test
app.dependency_overrides
zurückgesetzt wird (app.dependency_overrides = {}
), um sicherzustellen, dass diese Überschreibung andere Tests in Ihrer Suite nicht beeinträchtigt. Eine robustere Methode zur Handhabung dieser Probleme in Test-Fixtures ist oft vorzuziehen.
Fortgeschrittene Überschreibungstechniken (mit pytest
-Fixtures)
Für größere Test-Suiten kann das manuelle Festlegen und Löschen von app.dependency_overrides
umständlich werden. pytest
-Fixtures bieten eine sauberere Methode:
import pytest from fastapi.testclient import TestClient from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() def get_current_user_prod(token: str): if token == "prod-secret": return {"username": "prod_user", "id": 1} raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) @app.get("/protected/") def protected_route(user: dict = Depends(get_current_user_prod)): return {"message": f"Hallo, {user['username']}!"} # Mock-Abhängigkeit für Tests def get_current_user_mock(): return {"username": "test_user", "id": 99} @pytest.fixture(name="client") def test_client_fixture(): with TestClient(app) as client: yield client # Übergibt den Client an die Testfunktion @pytest.fixture(autouse=True) # "autouse=True" lässt dieses Fixture automatisch für alle Tests ausführen def override_get_current_user(): # Setzt die Überschreibung vor Ausführung des Tests app.dependency_overrides[get_current_user_prod] = get_current_user_mock yield # Lässt den Test laufen # Löscht die Überschreibung nach Abschluss des Tests app.dependency_overrides = {} def test_protected_route_with_mock_user(client): response = client.get("/protected/") assert response.status_code == 200 assert response.json() == {"message": "Hallo, test_user!"} # Wenn Sie die *ursprüngliche* Abhängigkeitslogik in bestimmten Tests testen möchten, # benötigen Sie einen Mechanismus, um das autouse-Fixture vorübergehend zu deaktivieren # oder eine andere Client-Instanz zu verwenden.
Diese pytest
-Einrichtung stellt sicher, dass get_current_user_prod
für jeden Test, der das client
-Fixture verwendet, automatisch durch get_current_user_mock
ersetzt wird und die Überschreibung nach jedem Test ordnungsgemäß bereinigt wird.
Fazit
FastAPIs Dependency Injection System ist eine leistungsstarke und elegante Lösung zum Erstellen skalierbarer, testbarer und wartbarer APIs. Durch die Nutzung von Python-Typ-Hints und dem Depends
-Dienstprogramm fördert es eine klare Trennung von Belangen, vereinfacht die Codeorganisation und verbessert die Testbarkeit erheblich. Das Verständnis seiner Prinzipien und die Beherrschung von Abhängigkeitsüberschreibungen sind entscheidend, um das volle Potenzial von FastAPI auszuschöpfen und Entwicklern zu ermöglichen, qualitativ hochwertige, robuste Python-Webanwendungen mit Zuversicht zu schreiben. Die Möglichkeit, Komponenten nahtlos für Tests auszutauschen, ist ein entscheidender Vorteil und festigt Dependency Injection als unverzichtbares Werkzeug in der modernen API-Entwicklungslandschaft.