Ponořte se hluboko do výkonného systému injekce závislostí FastAPI. Naučte se pokročilé techniky, vlastní závislosti, rozsahy a testovací strategie pro robustní vývoj API.
FastAPI Dependency System: Pokročilá injekce závislostí
Systém injekce závislostí (DI) FastAPI je základním kamenem jeho designu, podporuje modularitu, testovatelnost a opětovnou použitelnost. Zatímco základní použití je přímočaré, zvládnutí pokročilých technik DI odemyká významnou sílu a flexibilitu. Tento článek se zabývá pokročilou injekcí závislostí ve FastAPI, pokrývá vlastní závislosti, rozsahy, testovací strategie a osvědčené postupy.
Porozumění základům
Než se ponoříme do pokročilých témat, rychle si zopakujme základy injekce závislostí FastAPI:
- Závislosti jako funkce: Závislosti jsou deklarovány jako běžné funkce Pythonu.
- Automatická injekce: FastAPI automaticky injektuje tyto závislosti do operací s cestami na základě typových nápověd.
- Typové nápovědy jako kontrakty: Typové nápovědy definují očekávané vstupní typy pro závislosti a funkce operací s cestami.
- Hierarchické závislosti: Závislosti mohou záviset na jiných závislostech, čímž se vytváří strom závislostí.
Zde je jednoduchý příklad:
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
db = {"items": []}
try:
yield db
finally:
# Close the connection if needed
pass
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
V tomto příkladu je get_db závislost, která poskytuje databázové připojení. FastAPI automaticky volá get_db a injektuje výsledek do funkce read_items.
Pokročilé techniky závislostí
1. Použití tříd jako závislostí
Zatímco funkce se běžně používají, třídy mohou také sloužit jako závislosti, což umožňuje složitější správu stavu a metody. To je užitečné zejména při práci s databázovými připojeními, autentizačními službami nebo jinými zdroji, které vyžadují inicializaci a úklid.
from fastapi import FastAPI, Depends
app = FastAPI()
class Database:
def __init__(self):
self.connection = self.create_connection()
def create_connection(self):
# Simulate a database connection
print("Creating database connection...")
return {"items": []}
def close(self):
# Simulate closing a database connection
print("Closing database connection...")
def get_db():
db = Database()
try:
yield db.connection
finally:
db.close()
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
V tomto příkladu třída Database zapouzdřuje logiku databázového připojení. Závislost get_db vytvoří instanci třídy Database a vrátí připojení. Blok finally zajišťuje, že připojení je řádně uzavřeno po zpracování požadavku.
2. Přepisování závislostí
FastAPI umožňuje přepisovat závislosti, což je zásadní pro testování a vývoj. Můžete nahradit skutečnou závislost maketou nebo stubem, abyste izolovali svůj kód a zajistili konzistentní výsledky.
from fastapi import FastAPI, Depends
app = FastAPI()
# Original dependency
def get_settings():
# Simulate loading settings from a file or environment
return {"api_key": "real_api_key"}
@app.get("/items/")
async def read_items(settings: dict = Depends(get_settings)):
return {"api_key": settings["api_key"]}
# Override for testing
def get_settings_override():
return {"api_key": "test_api_key"}
app.dependency_overrides[get_settings] = get_settings_override
# To revert back to the original:
# del app.dependency_overrides[get_settings]
V tomto příkladu je závislost get_settings přepsána pomocí get_settings_override. To vám umožní použít jiný klíč API pro účely testování.
3. Použití `contextvars` pro data s rozsahem požadavku
contextvars je modul Pythonu, který poskytuje proměnné lokální pro kontext. To je užitečné pro ukládání dat specifických pro požadavek, jako jsou informace o autentizaci uživatele, ID požadavku nebo data trasování. Použití contextvars s injekcí závislostí FastAPI vám umožňuje přístup k těmto datům v celé vaší aplikaci.
import contextvars
from fastapi import FastAPI, Depends, Request
import uuid
app = FastAPI()
# Create a context variable for the request ID
request_id_var = contextvars.ContextVar("request_id")
# Middleware to set the request ID
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
request_id_var.set(request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
# Dependency to access the request ID
def get_request_id():
return request_id_var.get()
@app.get("/items/")
async def read_items(request_id: str = Depends(get_request_id)):
return {"request_id": request_id}
V tomto příkladu middleware nastaví jedinečné ID požadavku pro každý příchozí požadavek. Závislost get_request_id načte ID požadavku z kontextu contextvars. To vám umožní sledovat požadavky v celé vaší aplikaci.
4. Asynchronní závislosti
FastAPI bezproblémově podporuje asynchronní závislosti. To je zásadní pro neblokující I/O operace, jako jsou databázové dotazy nebo externí volání API. Jednoduše definujte svou funkci závislosti jako funkci async def.
from fastapi import FastAPI, Depends
import asyncio
app = FastAPI()
async def get_data():
# Simulate an asynchronous operation
await asyncio.sleep(1)
return {"message": "Hello from async dependency!"}
@app.get("/items/")
async def read_items(data: dict = Depends(get_data)):
return data
V tomto příkladu je závislost get_data asynchronní funkce, která simuluje zpoždění. FastAPI automaticky čeká na výsledek asynchronní závislosti před jejím injektováním do funkce read_items.
5. Použití generátorů pro správu zdrojů (databázová připojení, souborové handly)
Použití generátorů (s yield) poskytuje automatickou správu zdrojů, zaručující, že zdroje jsou řádně uzavřeny/uvolněny prostřednictvím bloku finally, i když dojde k chybám.
from fastapi import FastAPI, Depends
app = FastAPI()
def get_file_handle():
try:
file_handle = open("my_file.txt", "r")
yield file_handle
finally:
file_handle.close()
@app.get("/file_content/")
async def read_file_content(file_handle = Depends(get_file_handle)):
content = file_handle.read()
return {"content": content}
Rozsahy a životní cykly závislostí
Porozumění rozsahům závislostí je zásadní pro správu životního cyklu závislostí a zajištění řádného přidělování a uvolňování zdrojů. FastAPI nenabízí přímo explicitní anotace rozsahu, jako některé jiné DI frameworky (např. Spring's `@RequestScope`, `@ApplicationScope`), ale kombinace toho, jak definujete závislosti, a jak spravujete stav, dosahuje podobných výsledků.
Rozsah požadavku
Toto je nejběžnější rozsah. Každý požadavek obdrží novou instanci závislosti. Toho se obvykle dosahuje vytvořením nového objektu uvnitř funkce závislosti a jeho vrácením pomocí yield, jak je uvedeno v předchozím příkladu s databází. Použití contextvars také pomáhá dosáhnout rozsahu požadavku.
Rozsah aplikace (Singleton)
Vytvoří se jediná instance závislosti a sdílí se napříč všemi požadavky během životního cyklu aplikace. To se často provádí pomocí globálních proměnných nebo atributů na úrovni třídy.
from fastapi import FastAPI, Depends
app = FastAPI()
# Singleton instance
GLOBAL_SETTING = {"api_key": "global_api_key"}
def get_global_setting():
return GLOBAL_SETTING
@app.get("/items/")
async def read_items(setting: dict = Depends(get_global_setting)):
return setting
Buďte opatrní při používání závislostí s rozsahem aplikace s proměnlivým stavem, protože změny provedené jedním požadavkem mohou ovlivnit jiné požadavky. Pokud má vaše aplikace souběžné požadavky, mohou být nutné synchronizační mechanismy (zámky atd.).
Rozsah relace (data specifická pro uživatele)
Přidružte závislosti k uživatelským relacím. To vyžaduje mechanismus správy relací (např. pomocí souborů cookie nebo JWT) a obvykle zahrnuje ukládání závislostí do dat relace.
from fastapi import FastAPI, Depends, Cookie
from typing import Optional
import uuid
app = FastAPI()
# In a real app, store sessions in a database or cache
sessions = {}
async def get_user_id(session_id: Optional[str] = Cookie(None)) -> str:
if session_id is None or session_id not in sessions:
session_id = str(uuid.uuid4())
sessions[session_id] = {"user_id": str(uuid.uuid4())} # Assign a random user ID
return sessions[session_id]["user_id"]
@app.get("/profile/")
async def read_profile(user_id: str = Depends(get_user_id)):
return {"user_id": user_id}
Testování závislostí
Jednou z hlavních výhod injekce závislostí je zlepšená testovatelnost. Oddělením komponent můžete snadno nahradit závislosti maketami nebo stuby během testování.
1. Přepisování závislostí v testech
Jak bylo ukázáno dříve, mechanismus dependency_overrides FastAPI je ideální pro testování. Vytvořte makety závislostí, které vracejí předvídatelné výsledky, a použijte je k izolaci kódu pod testem.
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends
app = FastAPI()
# Original dependency
def get_external_data():
# Simulate fetching data from an external API
return {"data": "Real external data"}
@app.get("/data/")
async def read_data(data: dict = Depends(get_external_data)):
return data
# Test
from unittest.mock import MagicMock
def get_external_data_mock():
return {"data": "Mocked external data"}
def test_read_data():
app.dependency_overrides[get_external_data] = get_external_data_mock
client = TestClient(app)
response = client.get("/data/")
assert response.status_code == 200
assert response.json() == {"data": "Mocked external data"}
# Clean up overrides
app.dependency_overrides.clear()
2. Použití knihoven pro vytváření maket
Knihovny jako unittest.mock poskytují výkonné nástroje pro vytváření maket objektů a řízení jejich chování. Můžete použít makety k simulaci složitých závislostí a ověřit, zda váš kód s nimi správně interaguje.
import unittest
from unittest.mock import MagicMock
# (Define the FastAPI app and get_external_data as above)
class TestReadData(unittest.TestCase):
def test_read_data_with_mock(self):
# Create a mock for the get_external_data dependency
mock_get_external_data = MagicMock(return_value={"data": "Mocked data from unittest"})
# Override the dependency with the mock
app.dependency_overrides[get_external_data] = mock_get_external_data
client = TestClient(app)
response = client.get("/data/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"data": "Mocked data from unittest"})
# Assert that the mock was called
mock_get_external_data.assert_called_once()
# Clean up overrides
app.dependency_overrides.clear()
3. Injekce závislostí pro unit testování (mimo kontext FastAPI)
I při unit testování funkcí *mimo* obslužné rutiny koncového bodu API se stále uplatňují principy injekce závislostí. Místo spoléhání se na `Depends` FastAPI ručně injektujte závislosti do testované funkce.
# Example function to test
def process_data(data_source):
data = data_source.fetch_data()
# ... process the data ...
return processed_data
class MockDataSource:
def fetch_data(self):
return {"example": "data"}
# Unit test
def test_process_data():
mock_data_source = MockDataSource()
result = process_data(mock_data_source)
# Assertions on the result
Bezpečnostní aspekty injekce závislostí
Injekce závislostí, i když je prospěšná, zavádí potenciální bezpečnostní rizika, pokud není implementována opatrně.
1. Zmatení závislostí
Ujistěte se, že stahujete závislosti z důvěryhodných zdrojů. Ověřte integritu balíčku a používejte správce balíčků se schopnostmi skenování zranitelností. Toto je obecný princip zabezpečení dodavatelského řetězce softwaru, ale injekce závislostí jej zhoršuje, protože můžete injektovat komponenty z různých zdrojů.
2. Injekce škodlivých závislostí
Dávejte pozor na závislosti, které přijímají externí vstup bez řádného ověření. Útočník by mohl potenciálně injektovat škodlivý kód nebo data prostřednictvím ohrožené závislosti. Očistěte všechny vstupy uživatele a implementujte robustní mechanismy ověřování.
3. Únik informací prostřednictvím závislostí
Ujistěte se, že závislosti neúmyslně nezpřístupňují citlivé informace. Zkontrolujte kód a konfiguraci svých závislostí, abyste identifikovali potenciální zranitelnosti úniku informací.
4. Pevně zakódovaná tajemství
Vyhněte se pevnému zakódování tajemství (klíče API, hesla databáze atd.) přímo do kódu závislosti. Používejte proměnné prostředí nebo zabezpečené nástroje pro správu konfigurace k ukládání a správě tajemství.
import os
from fastapi import FastAPI, Depends
app = FastAPI()
def get_api_key():
api_key = os.environ.get("API_KEY")
if not api_key:
raise ValueError("API_KEY environment variable not set.")
return api_key
@app.get("/secure_endpoint/")
async def secure_endpoint(api_key: str = Depends(get_api_key)):
# Use api_key for authentication/authorization
return {"message": "Access granted"}
Optimalizace výkonu pomocí injekce závislostí
Injekce závislostí může ovlivnit výkon, pokud není používána uvážlivě. Zde je několik strategií optimalizace:
1. Minimalizujte náklady na vytváření závislostí
Pokud je to možné, vyhněte se vytváření nákladných závislostí při každém požadavku. Pokud je závislost bezstavová nebo ji lze sdílet napříč požadavky, zvažte použití rozsahu singletonu nebo ukládání instance závislosti do mezipaměti.
2. Líná inicializace
Inicializujte závislosti pouze v případě, že jsou potřeba. To může snížit dobu spouštění a spotřebu paměti, zejména u aplikací s mnoha závislostmi.
3. Ukládání výsledků závislostí do mezipaměti
Ukládejte do mezipaměti výsledky nákladných výpočtů závislostí, pokud je pravděpodobné, že budou výsledky znovu použity. Použijte mechanismy ukládání do mezipaměti (např. Redis, Memcached) k ukládání a načítání výsledků závislostí.
4. Optimalizujte graf závislostí
Analyzujte svůj graf závislostí, abyste identifikovali potenciální úzká hrdla. Zjednodušte strukturu závislostí a snižte počet závislostí, pokud je to možné.
5. Asynchronní závislosti pro I/O vázané operace
Používejte asynchronní závislosti při provádění blokujících I/O operací, jako jsou databázové dotazy nebo externí volání API. Tím se zabrání blokování hlavního vlákna a zlepší se celková odezva aplikace.
Osvědčené postupy pro injekci závislostí FastAPI
- Udržujte závislosti jednoduché: Zaměřte se na malé, zaměřené závislosti, které provádějí jeden úkol. To zlepšuje čitelnost, testovatelnost a udržovatelnost.
- Používejte typové nápovědy: Využijte typové nápovědy k jasnému definování očekávaných vstupních a výstupních typů závislostí. To zlepšuje srozumitelnost kódu a umožňuje FastAPI provádět statickou typovou kontrolu.
- Dokumentujte závislosti: Dokumentujte účel a použití každé závislosti. To pomáhá ostatním vývojářům pochopit, jak používat a udržovat váš kód.
- Důkladně testujte závislosti: Pište unit testy pro své závislosti, abyste zajistili, že se chovají podle očekávání. To pomáhá předcházet chybám a zlepšuje celkovou spolehlivost vaší aplikace.
- Používejte konzistentní konvence pojmenování: Používejte konzistentní konvence pojmenování pro své závislosti, abyste zlepšili čitelnost kódu.
- Vyhněte se cyklickým závislostem: Cyklické závislosti mohou vést ke složitému a obtížně laditelnému kódu. Refaktorujte svůj kód, abyste eliminovali cyklické závislosti.
- Zvažte kontejnery injekce závislostí (volitelné): Zatímco vestavěná injekce závislostí FastAPI je dostatečná pro většinu případů, zvažte použití vyhrazeného kontejneru injekce závislostí (např. `inject`, `autowire`) pro složitější aplikace.
Závěr
Systém injekce závislostí FastAPI je výkonný nástroj, který podporuje modularitu, testovatelnost a opětovnou použitelnost. Zvládnutím pokročilých technik, jako je použití tříd jako závislostí, přepisování závislostí a použití contextvars, můžete vytvářet robustní a škálovatelné API. Porozumění rozsahům a životním cyklům závislostí je zásadní pro efektivní správu zdrojů. Vždy upřednostňujte důkladné testování svých závislostí, abyste zajistili spolehlivost a bezpečnost svých aplikací. Dodržováním osvědčených postupů a zvažováním potenciálních bezpečnostních a výkonnostních dopadů můžete využít plný potenciál systému injekce závislostí FastAPI.