La dependency injection è un pattern di progettazione in cui oggetti o servizi (cioè le dipendenze) vengono forniti a una classe dall'esterno, invece che essere creati internamente dalla classe stessa. Questo disaccoppia i componenti.
Usare la dependency injection in Python ha molti vantaggi, tra cui:
Modularità: il codice è suddiviso in parti più piccole e riutilizzabili.
Testabilità: è più facile testare il codice perché puoi sostituire la parte reale con dei mock.
Manutenibilità: puoi aggiornare o modificare parti del codice senza rompere il resto del sistema.
In questo tutorial ti mostrerò come usare la dependency injection in Python con esempi semplici e pratici.
TL;DR
La dependency injection è un pattern in cui una classe riceve le sue dipendenze dall'esterno invece di crearle da sola. Rende il codice più modulare.
Puoi iniettare dipendenze tramite il costruttore, un metodo setter o direttamente in un metodo; Python lo supporta manualmente o tramite framework come dependency-injector, injector e punq.
I framework web più diffusi la gestiscono in modo diverso: FastAPI ha il supporto integrato tramite Depends(). Flask e Django richiedono una libreria come dependency-injector.
Capire la Dependency Injection in Python
Per capire come implementare la dependency injection, è essenziale afferrare i principi chiave che la governano.
La dependency injection si basa sul principio dell'Inversion of Control (IoC), che significa che, invece di creare e gestire le proprie dipendenze, una classe le riceve da una fonte esterna. Questo aiuta a focalizzarsi su un singolo compito e rende il codice pulito.
L'iniezione delle dipendenze in Python include questi metodi comuni, sebbene ce ne siano altri:
Constructor injection: passa le dipendenze tramite il costruttore della classe.
Setter injection: imposta le dipendenze usando un metodo setter dopo la creazione dell'oggetto.
Method injection: passa le dipendenze direttamente al metodo che ne ha bisogno.
Perché usare la Dependency Injection in Python?
Come sviluppatore, comprendere e implementare la dependency injection può fare una grande differenza nel design del tuo codice. Vediamo alcuni vantaggi della dependency injection.
Modularità, riusabilità e flessibilità
La dependency injection ti consente di suddividere il codice in componenti più piccoli e mirati, offrendo maggiore flessibilità e modularità.
Ogni parte o componente gestisce un compito specifico e si affida a dipendenze esterne, il che ne facilita il riuso in parti diverse della tua applicazione o di quella di altri
Per esempio, se hai una classe che invia messaggi, invece di inserire direttamente la logica per email o SMS nel servizio, puoi iniettare una classe di invio email o SMS.
Ora la tua classe che invia messaggi non si preoccupa di come il messaggio venga inviato, ma usa semplicemente il sender che le viene fornito. Questo implica che puoi usare il tuo sender con diversi invii email o SMS.
Manutenibilità e testabilità
Con la dependency injection puoi sostituire facilmente le dipendenze reali con dati mock durante i test. Questo semplifica la scrittura di unit test senza dipendere da API o database.
Supponiamo, come esercizio mentale, di avere una classe che salva dati in un database. Per i test, puoi iniettare un database mock invece di uno reale per evitare di accoppiare il test al database di produzione.
La dependency injection ti permette di modificare parti specifiche del codice senza rompere l'intero sistema. È particolarmente utile quando devi cambiare provider di pagamento in un'applicazione, ad esempio da Stripe a PayPal, senza modificare il resto del codice.
Inversion of control e dipendenze intercambiabili
L'idea dell'inversion of control è questa: la dependency injection sposta la responsabilità della creazione e gestione delle dipendenze su una fonte esterna.
Questo implica che puoi sostituire facilmente un'implementazione con un'altra; per esempio, se hai una classe ReportGenerator che formatta ed esporta report.
Invece che sia la classe a decidere il formato di esportazione, puoi iniettarle una classe di esportazione, come PDFExporter, ExcelExporter o CSVExporter.
La classe ReportGenerator non si cura di come avvenga l'esportazione, quindi per cambiare formato ti basta sostituire l'exporter che inietti, senza modificare affatto ReportGenerator.
Implementare la Dependency Injection in Python
Ora che hai un'idea chiara di cos'è la dependency injection e perché è utile, vediamo come implementarla in Python. Partiamo dal metodo più semplice: la dependency injection manuale.
Dependency injection manuale
Con la dependency injection manuale, crei tu stesso le dipendenze e le passi alla classe o funzione che ne ha bisogno.
Questo metodo funziona bene per applicazioni piccole, ma quando l'app cresce dovresti usare un framework di dependency injection per evitare errori.
Ecco un esempio di codice senza 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!")Offerto da
Nell'esempio sopra, UserNotifier è strettamente accoppiata a EmailService. Non puoi sostituire o fare facilmente il mock di EmailService per i test.
Ecco un'altra versione, ma con dependency injection manuale.
Container definisce come vengono create le istanze
Singleton garantisce che esista una sola istanza di EmailService
Factory crea un nuovo UserNotifier con EmailService iniettato automaticamente.
Meccanismo di wiring
Puoi usare decorator come @inject per ridurre la necessità di passare manualmente le dipendenze.
Ecco un'altra versione dell'esempio precedente:
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()Offerto da
Nell'attuale versione:
Senza aggiungere manualmente la dipendenza, il decorator @inject inietta automaticamente la dipendenza della funzione o classe.
La classe Provide indica a dependency-injector quale dipendenza iniettare.
Questo approccio elimina la necessità di collegare manualmente le dipendenze dal container.
È utile in applicazioni più grandi in cui i servizi possono dipendere da più componenti (quindi la necessità di inizializzarli automaticamente).
Usare la Dependency Injection nei framework Python più diffusi
La maggior parte dei framework Python moderni facilita l'uso della dependency injection, permettendoti di scrivere codice più pulito e più facile da testare.
In questa sezione vedrai come si applica la dependency injection in Flask, Django e FastAPI.
Dependency injection in Flask
Flask non ha il supporto integrato per la dependency injection, ma puoi implementarla usando la libreria dependency-injector seguendo questi passaggi.
Passo 1: definisci i tuoi servizi
classGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Offerto da
Passo 2: configura il container di dependency injection
from dependency_injector import containers, providers
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Offerto da
Passo 3: crea l'applicazione Flask e inietta le dipendenze.
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)Offerto da
Dal codice sopra:
La funzione name = request.args.get(“name”, “World”) estrae il name dalla query string e usa ”World” come default se non è fornito.
return greeting_service.get_greeting(name) chiama il metodo sul servizio iniettato e restituisce un messaggio di saluto.
container.wire(modules=[__name__]) dice al dependency injector di scansionare questo modulo per i decorator @inject e i marker Provide[...] per collegarli ai provider effettivi.
Passo 4: testa l'applicazione
Esegui l'applicazione dal terminale con il comando python <name_of_file>.py e visita l'URL http://localhost:5000/greet?name=Jacob.
Dovresti vedere quanto segue.
Dependency injection in Django
Proprio come Flask, anche Django non offre la dependency injection integrata. Tuttavia, puoi usare le class-based view, il middleware e la struttura delle app per integrarla manualmente o con la libreria dependency-injector.
Ecco una procedura passo passo per implementare la dependency injection in Django.
Passo 1: definisci il tuo servizio
Crea un servizio semplice con la tua logica di business in un nuovo file services.py nella cartella della tua app.
# myapp/services.pyclassGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Offerto da
Passo 2: configura il container del dependency injector
All'interno della directory dell'applicazione, crea un container usando la libreria dependency-injector in un nuovo file containers.py.
Usa il decorator @inject nella tua view per ricevere le dipendenze 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)Offerto da
Passo 4: aggiorna urls.py
Nella cartella del progetto, vai al file urls.py e collega il dependency injector al sistema di routing di Django.
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),]Offerto da
Passo 5: collega il container in apps.py
Vai in apps.py e collega il container all'applicazione.
# 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"])Offerto da
Poi vai nella configurazione dell'app in INSTALLED_APPS e aggiungi la tua applicazione.
# settings.py
INSTALLED_APPS =['myapp.apps.MyAppConfig',...]Offerto da
Passo 6: testa la View
Avvia il server Django e accedi alla view tramite l'URL http://localhost:8000/greet/?name=Django.
Dovresti vedere quanto segue.
Dependency Injection in FastAPI
FastAPI ha il supporto integrato per la dependency injection tramite il marker speciale Depends() che inietta qualunque cosa restituisca una funzione.
Ecco una guida passo passo su come procedere con la dependency injection in FastAPI.
Passo 1: definisci un servizio
classGreetingService:defget_greeting(self, name:str)->str:returnf"Hello, {name}!"Offerto da
Passo 2: crea la funzione di dipendenza
defget_greeting_service():return GreetingService()Offerto da
Passo 3: configura l'app FastAPI e usa la Dependency Injection
app = FastAPI()@app.get("/greet")defgreet(
name:str="World", service: GreetingService = Depends(get_greeting_service)):return{"message": service.get_greeting(name)}Offerto da
Passo 4: esegui e testa l'applicazione
Avvia il server con il comando uvicorn main:app –reload e visita l'URL http://localhost:8000/greet?name=FastAPI. Dovresti vedere la seguente risposta.
Confronto tra framework di Dependency Injection per Python
Esistono vari framework di dependency injection per Python, ognuno con i propri punti di forza, sintassi e casi d'uso. La scelta del framework giusto dipende dalla dimensione e dalla struttura del progetto.
Ecco un confronto tra alcuni framework di dependency injection comuni in Python.
Fornisce dependency injection per view e middleware
Dipende da injector
Pattern di implementazione avanzati
Quando la codebase della tua applicazione cresce, la dependency injection diventa difficile da mantenere.
Puoi usare tecniche avanzate di dependency injection per gestire attività come risorse con scope di richiesta, cleanup e separazione delle responsabilità.
Dipendenze con scope
Le dipendenze con scope ti permettono di controllare quanto a lungo persiste una dipendenza, ad esempio quando vuoi un nuovo oggetto per richiesta, un oggetto condiviso per l'intero ciclo di vita dell'app o il cleanup automatico di risorse come le connessioni al database.
La maggior parte dei framework di dependency injection supporta scope come:
Singleton: un'istanza per l'intera app.
Factory: una nuova istanza ogni volta che viene iniettata.
Thread-local o request: un'unica istanza per thread/richiesta.
Resource: gestita con logica di setup e teardown.
Di seguito un esempio di dipendenze con scope con container annidati usando 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")Offerto da
Nell'esempio sopra, MyResource imita una classe che richiede setup e teardown, come aprire/chiudere un file o un database.
Il container fornisce la risorsa usando providers.Resource e quando usi with container.resource() entra ed esce correttamente dal contesto.
Questo pattern garantisce che risorse come file handle, connessioni al database o sessioni di rete siano gestite in modo pulito.
Immagina un endpoint API che ha bisogno di una sessione di database per la richiesta corrente o di un logger con contesto specifico della richiesta.
Con le dipendenze con scope, il framework crea un container con scope di richiesta quando la richiesta inizia, risolvendo tutte le dipendenze da quello scope.
Quando la richiesta termina, eventuali cleanup necessari, come la chiusura delle sessioni di database, avvengono automaticamente. Questo assicura che gli stati non siano condivisi tra richieste ed evita perdite di risorse.
Iniezione asincrona
Per gestire in modo efficiente i task I/O-bound, hai bisogno dell'esecuzione asincrona. I sistemi di dependency injection in framework come FastAPI e dependency-injector offrono l'iniezione asincrona usando async/await.
Per iniezione asincrona si intende l'iniezione di dipendenze che sono funzioni async def o la gestione di async resources come connessioni a database asincroni, API esterne o task in background.
Invece di risolverle in modo sincrono, il sistema di dependency injection attende che vengano risolte prima di iniettarle.
Di seguito un esempio di dipendenza async 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}Offerto da
FastAPI gestisce automaticamente la risoluzione async di get_user e ne inietta il risultato.
dependency-injector supporta anche la gestione del ciclo di vita async usando i provider AsyncResource.
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()Offerto da
Nel codice sopra:
resources.AsyncResource è usato per la logica di setup/teardown asincrona.
await container.init_resources() inizializza tutte le risorse e chiama init.
await container.shutdown_resources() pulisce tutte le risorse.
await container.db() ottiene l'istanza reale della risorsa, cioè AsyncDB.
Usare l'iniezione async aiuta a mantenere l'app performante evitando I/O sincrono in route asincrone, ed elimina la necessità di attendere manualmente le dipendenze nella tua logica.
Implicazioni di sicurezza
Sebbene la dependency injection migliori modularità e testabilità, può introdurre rischi di sicurezza se non gestita con attenzione.
Un rischio significativo è la dependency confusion, in cui pacchetti malevoli imitano quelli legittimi. Per esempio, alcuni attaccanti hanno caricato pacchetti su PyPI con nomi usati in sistemi interni aziendali per indurre le app a usare la versione malevola.
Per restare al sicuro, assicurati di non iniettare mai componenti non attendibili o controllati dall'utente e valida le fonti di configurazione, come le variabili d'ambiente, prima che vengano iniettate.
Usa file requirements.txt o poetry.lock per bloccare le versioni esatte, e automatizza la scansione di sicurezza quando cambiano le dipendenze. Inoltre, verifica regolarmente le tue dipendenze per vulnerabilità.
Centralizzare il controllo delle dipendenze è essenziale, perché assicura coerenza tra le applicazioni e facilita l'applicazione di patch di sicurezza o aggiornamenti di versione. Aiuta a evitare "dipendenze ombra" iniettate in modi diversi nella codebase.
Diversi strumenti possono rilevare rischi e migliorare la tua postura di sicurezza:
Snyk: analizza i pacchetti Python per vulnerabilità note e suggerisce correzioni.
Safety: controlla pacchetti insicuri e vulnerabilità del codice.
Bandit: un analizzatore statico del codice per Python focalizzato su problemi di sicurezza.
Applicazioni reali
La dependency injection non è solo un pattern teorico; viene usata in vari sistemi di produzione per gestire la complessità, migliorare la flessibilità e semplificare i test.
Configurazione dei servizi web
Nelle applicazioni web moderne, la dependency injection ha un ruolo cruciale nella gestione di servizi standard come autenticazione, logging e accesso al database.
Framework come FastAPI svolgono un ruolo essenziale nella risoluzione di route e dipendenze.
L'esempio seguente illustra un'implementazione centralizzata dell'autenticazione 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']}"}Offerto da
Nel codice sopra, get_current_user() è iniettata come dipendenza della route, ma la logica di verifica del token è centralizzata e riutilizzabile.
Puoi fare lo stesso per l'accesso al database, in modo che le sessioni siano gestite e pulite automaticamente.
Ecco un esempio di come usare la dependency injection per centralizzare l'accesso al database.
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")Offerto da
Nel codice sopra, get_db_session è un provider di dipendenze. Quando viene chiamata la route read_items, FastAPI inietta automaticamente il parametro db, consentendo alla route di accedere al database senza gestire manualmente setup o teardown della sessione.
Test-driven development
La dependency injection rende facile cambiare le dipendenze che i componenti ricevono, il che è importante in fase di test.
Puoi sostituire i servizi reali con mock durante i test, rendendo il codice più modulare e più facile da testare.
FastAPI e dependency-injector supportano la sostituzione delle dipendenze in fase di test.
Ecco un esempio 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 ==200Offerto da
Nell'esempio di codice sopra, get_db_session è sovrascritta con un database mock durante i test; quindi non c'è bisogno di modificare il codice di produzione.
Anche in depedency-injector puoi sostituire il tuo database reale con un database mock per i test, come nell'esempio seguente.
from containers import Container
deftest_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()assert service.get_data()=="mocked"Offerto da
Il metodo override() sostituisce la dipendenza reale con un mock, in modo pulito, controllato e reversibile.
Ecco alcune strategie da usare per le override delle dipendenze:
Constructor injection: assicurati di passare i mock direttamente al costruttore.
Override del framework: usa gli strumenti di override integrati come FastAPI.dependency_overrides in FastAPI o dependency_injector.override() in dependency-injector.
Fixture: usa framework di testing come pytest per creare mock riutilizzabili e iniettarli tramite fixture.
Best practice e anti-pattern
Sebbene la dependency injection sia pensata per rendere il codice modulare e facile da testare, può portare a bug o creare complessità nel codice.
Pratiche raccomandate
Ecco alcune best practice da seguire quando crei dipendenze:
Programma verso interfacce, non implementazioni: definisci le dipendenze usando classi astratte o interfacce anziché implementazioni complesse. Rende il codice più facile da testare e incoraggia l'uso di mock nei test unitari.
Preferisci la composizione all'ereditarietà: piuttosto che costruire gerarchie di classi complesse, componi oggetti usando dipendenze iniettate per mantenere i componenti piccoli e focalizzati.
Configurazione centralizzata delle dipendenze: utilizza un unico container o modulo per definire e gestire le dipendenze, facilitando override e test.
Evita le dipendenze circolari: progetta con cura il grafo delle dipendenze per prevenire riferimenti circolari usando funzioni factory o provider per ritardare l'istanziazione.
Errori comuni
Ecco alcune insidie comuni da evitare quando usi la dependency injection.
Anti-pattern del service locator: evita di usare un container globale per recuperare dinamicamente le dipendenze, perché può nasconderle e rendere il codice difficile da capire e testare.
Over-injection: evita di iniettare troppi servizi in una singola classe o funzione per non avere costruttori gonfi e logica difficile da testare.
Gestione scorretta dello scope e perdita di stato: usa sempre lo scope corretto per evitare condivisione di stato tra utenti o test.
Testing della Dependency Injection in Python
La dependency injection rende facile iniettare oggetti mock nei componenti, semplificando così gli unit test. Ciò ti permette di separare i componenti fondamentali da quelli specifici per i test.
Con la dependency injection, i test sono deterministici, senza effetti collaterali e si eseguono più velocemente.
Ecco un esempio che mostra come usare la Dependency Injection per iniettare versioni mock, stub o fake di tali dipendenze nei test.
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]"Offerto da
Assicurati di usare spesso la dependency injection ed evita il monkey patching per la manutenibilità e per evitare rotture quando cambiano i nomi interni.
Conclusione
Anche se potresti non aver bisogno della dependency injection inizialmente per progetti piccoli, man mano che il progetto cresce e scala, puoi beneficiare della struttura e della modularità offerte dalla dependency injection.
Guardando avanti, il futuro della dependency injection in Python è promettente, e possiamo aspettarci un'evoluzione con un sistema di tipi migliorato e una gestione del ciclo di vita async potenziata.
Padroneggiare la dependency injection è essenziale per diventare uno sviluppatore Python fluente. Ma non finisce qui: molti altri concetti avanzati possono elevare le tue skill in Python.
Per aiutarti ad accelerare il percorso di apprendimento, ecco alcune delle nostre risorse da consultare.
Docente di data science esperto e biostatistico con competenze in Python, R e machine learning.
FAQs
Che cos'è la dependency injection?⌄
La dependency injection è un pattern di progettazione che ti permette di scrivere codice più pulito creando dipendenze all'interno di una classe che possono essere passate dall'esterno, rendendo il codice più facile da gestire e modificare.
Quali sono i principali vantaggi della dependency injection?⌄
La dependency injection rende il codice modulare, testabile e manutenibile.
Come posso implementare la dependency injection in Python?⌄
Puoi implementare la dependency injection in Python manualmente oppure usando librerie come dependency_injector.
Che cos'è l'iniezione asincrona?⌄
Per iniezione asincrona si intende l'iniezione di dipendenze che sono funzioni async def o la gestione di async resources come connessioni a database asincroni, API esterne o task in background.
Quali sono i rischi di sicurezza della dependency injection e come gestirli?⌄
Un rischio significativo è la dependency confusion, in cui pacchetti malevoli imitano quelli legittimi. Per prevenirla, puoi usare strumenti come Snyk, Xygeni, Safety o Bandit per analizzare codice e pacchetti alla ricerca di vulnerabilità.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
La dependency injection è un pattern di progettazione in cui oggetti o servizi (cioè le dipendenze) vengono forniti a una classe dall'esterno, invece che essere creati internamente dalla classe stessa. Questo disaccoppia i componenti.
Usare la dependency injection in Python ha molti vantaggi, tra cui:
In questo tutorial ti mostrerò come usare la dependency injection in Python con esempi semplici e pratici.
TL;DR
La dependency injection è un pattern in cui una classe riceve le sue dipendenze dall'esterno invece di crearle da sola. Rende il codice più modulare.
Puoi iniettare dipendenze tramite il costruttore, un metodo setter o direttamente in un metodo; Python lo supporta manualmente o tramite framework come
dependency-injector,injectorepunq.I framework web più diffusi la gestiscono in modo diverso: FastAPI ha il supporto integrato tramite
Depends(). Flask e Django richiedono una libreria comedependency-injector.Capire la Dependency Injection in Python
Per capire come implementare la dependency injection, è essenziale afferrare i principi chiave che la governano.
La dependency injection si basa sul principio dell'Inversion of Control (IoC), che significa che, invece di creare e gestire le proprie dipendenze, una classe le riceve da una fonte esterna. Questo aiuta a focalizzarsi su un singolo compito e rende il codice pulito.
L'iniezione delle dipendenze in Python include questi metodi comuni, sebbene ce ne siano altri:
Perché usare la Dependency Injection in Python?
Come sviluppatore, comprendere e implementare la dependency injection può fare una grande differenza nel design del tuo codice. Vediamo alcuni vantaggi della dependency injection.
Modularità, riusabilità e flessibilità
La dependency injection ti consente di suddividere il codice in componenti più piccoli e mirati, offrendo maggiore flessibilità e modularità.
Ogni parte o componente gestisce un compito specifico e si affida a dipendenze esterne, il che ne facilita il riuso in parti diverse della tua applicazione o di quella di altri
Per esempio, se hai una classe che invia messaggi, invece di inserire direttamente la logica per email o SMS nel servizio, puoi iniettare una classe di invio email o SMS.
Ora la tua classe che invia messaggi non si preoccupa di come il messaggio venga inviato, ma usa semplicemente il sender che le viene fornito. Questo implica che puoi usare il tuo sender con diversi invii email o SMS.
Manutenibilità e testabilità
Con la dependency injection puoi sostituire facilmente le dipendenze reali con dati mock durante i test. Questo semplifica la scrittura di unit test senza dipendere da API o database.
Supponiamo, come esercizio mentale, di avere una classe che salva dati in un database. Per i test, puoi iniettare un database mock invece di uno reale per evitare di accoppiare il test al database di produzione.
La dependency injection ti permette di modificare parti specifiche del codice senza rompere l'intero sistema. È particolarmente utile quando devi cambiare provider di pagamento in un'applicazione, ad esempio da Stripe a PayPal, senza modificare il resto del codice.
Inversion of control e dipendenze intercambiabili
L'idea dell'inversion of control è questa: la dependency injection sposta la responsabilità della creazione e gestione delle dipendenze su una fonte esterna.
Questo implica che puoi sostituire facilmente un'implementazione con un'altra; per esempio, se hai una classe
ReportGeneratorche formatta ed esporta report.Invece che sia la classe a decidere il formato di esportazione, puoi iniettarle una classe di esportazione, come
PDFExporter,ExcelExporteroCSVExporter.La classe
ReportGeneratornon si cura di come avvenga l'esportazione, quindi per cambiare formato ti basta sostituire l'exporter che inietti, senza modificare affattoReportGenerator.Implementare la Dependency Injection in Python
Ora che hai un'idea chiara di cos'è la dependency injection e perché è utile, vediamo come implementarla in Python. Partiamo dal metodo più semplice: la dependency injection manuale.
Dependency injection manuale
Con la dependency injection manuale, crei tu stesso le dipendenze e le passi alla classe o funzione che ne ha bisogno.
Questo metodo funziona bene per applicazioni piccole, ma quando l'app cresce dovresti usare un framework di dependency injection per evitare errori.
Ecco un esempio di codice senza dependency injection
Nell'esempio sopra,
UserNotifierè strettamente accoppiata aEmailService. Non puoi sostituire o fare facilmente il mock diEmailServiceper i test.Ecco un'altra versione, ma con dependency injection manuale.
La versione sopra è flessibile e ti consente di iniettare vari servizi di messaggistica; puoi anche sostituire
EmailServicecon un mock nei test.Usare il framework dependency-injector
Quando l'applicazione cresce, gestire le dipendenze diventa complesso e disordinato.
Il framework
dependency-injectorfornisce un approccio strutturato.Architettura container-provider
Il framework
dependency-injectorfunziona secondo un'architettura container-provider.Nell'esempio sopra:
Containerdefinisce come vengono create le istanzeSingletongarantisce che esista una sola istanza diEmailServiceFactorycrea un nuovoUserNotifierconEmailServiceiniettato automaticamente.Meccanismo di wiring
Puoi usare decorator come
@injectper ridurre la necessità di passare manualmente le dipendenze.Ecco un'altra versione dell'esempio precedente:
Nell'attuale versione:
Senza aggiungere manualmente la dipendenza, il decorator
@injectinietta automaticamente la dipendenza della funzione o classe.La classe
Provideindica adependency-injectorquale dipendenza iniettare.Questo approccio elimina la necessità di collegare manualmente le dipendenze dal container.
È utile in applicazioni più grandi in cui i servizi possono dipendere da più componenti (quindi la necessità di inizializzarli automaticamente).
Usare la Dependency Injection nei framework Python più diffusi
La maggior parte dei framework Python moderni facilita l'uso della dependency injection, permettendoti di scrivere codice più pulito e più facile da testare.
In questa sezione vedrai come si applica la dependency injection in Flask, Django e FastAPI.
Dependency injection in Flask
Flask non ha il supporto integrato per la dependency injection, ma puoi implementarla usando la libreria
dependency-injectorseguendo questi passaggi.Passo 1: definisci i tuoi servizi
Passo 2: configura il container di dependency injection
Passo 3: crea l'applicazione Flask e inietta le dipendenze.
Dal codice sopra:
La funzione
name = request.args.get(“name”, “World”)estrae ilnamedalla query string e usa”World”come default se non è fornito.return greeting_service.get_greeting(name)chiama il metodo sul servizio iniettato e restituisce un messaggio di saluto.container.wire(modules=[__name__])dice al dependency injector di scansionare questo modulo per i decorator@injecte i markerProvide[...]per collegarli ai provider effettivi.Passo 4: testa l'applicazione
Esegui l'applicazione dal terminale con il comando
python <name_of_file>.pye visita l'URLhttp://localhost:5000/greet?name=Jacob.Dovresti vedere quanto segue.
Dependency injection in Django
Proprio come Flask, anche Django non offre la dependency injection integrata. Tuttavia, puoi usare le class-based view, il middleware e la struttura delle app per integrarla manualmente o con la libreria
dependency-injector.Ecco una procedura passo passo per implementare la dependency injection in Django.
Passo 1: definisci il tuo servizio
Crea un servizio semplice con la tua logica di business in un nuovo file
services.pynella cartella della tua app.Passo 2: configura il container del dependency injector
All'interno della directory dell'applicazione, crea un container usando la libreria
dependency-injectorin un nuovo filecontainers.py.Passo 3: crea una View con dipendenze iniettate
Usa il decorator
@injectnella tua view per ricevere le dipendenze inviews.py.Passo 4: aggiorna urls.py
Nella cartella del progetto, vai al file
urls.pye collega il dependency injector al sistema di routing di Django.Passo 5: collega il container in apps.py
Vai in
apps.pye collega il container all'applicazione.Poi vai nella configurazione dell'app in
INSTALLED_APPSe aggiungi la tua applicazione.Passo 6: testa la View
Avvia il server Django e accedi alla view tramite l'URL
http://localhost:8000/greet/?name=Django.Dovresti vedere quanto segue.
Dependency Injection in FastAPI
FastAPI ha il supporto integrato per la dependency injection tramite il marker speciale
Depends()che inietta qualunque cosa restituisca una funzione.Ecco una guida passo passo su come procedere con la dependency injection in FastAPI.
Passo 1: definisci un servizio
Passo 2: crea la funzione di dipendenza
Passo 3: configura l'app FastAPI e usa la Dependency Injection
Passo 4: esegui e testa l'applicazione
Avvia il server con il comando
uvicorn main:app –reloade visita l'URLhttp://localhost:8000/greet?name=FastAPI. Dovresti vedere la seguente risposta.Confronto tra framework di Dependency Injection per Python
Esistono vari framework di dependency injection per Python, ognuno con i propri punti di forza, sintassi e casi d'uso. La scelta del framework giusto dipende dalla dimensione e dalla struttura del progetto.
Ecco un confronto tra alcuni framework di dependency injection comuni in Python.
Framework
Migliore per
Punti di forza
Limitazioni
dependency-injectorApplicazioni complesse in produzione
Completo di funzionalità, veloce e configurabile
Verboso
injectorSemplice e adatto ad applicazioni minime
Sintassi pulita
Meno avanzato di altri
pinjectLogica di binding avanzata
Auto-wiring
Più lento a causa dell'introspezione
punqApplicazioni semplici
Minimale e veloce
Funzionalità limitate rispetto ad altri
FastAPI
Depends()Applicazioni FastAPI
Supporto integrato per async, e anche testabile
Non riutilizzabile fuori da FastAPI
Flask-injectorApplicazioni Flask
Integrazione semplice con applicazioni Flask
Dipende da
injectordjango-injectorProgetti Django
Fornisce dependency injection per view e middleware
Dipende da
injectorPattern di implementazione avanzati
Quando la codebase della tua applicazione cresce, la dependency injection diventa difficile da mantenere.
Puoi usare tecniche avanzate di dependency injection per gestire attività come risorse con scope di richiesta, cleanup e separazione delle responsabilità.
Dipendenze con scope
Le dipendenze con scope ti permettono di controllare quanto a lungo persiste una dipendenza, ad esempio quando vuoi un nuovo oggetto per richiesta, un oggetto condiviso per l'intero ciclo di vita dell'app o il cleanup automatico di risorse come le connessioni al database.
La maggior parte dei framework di dependency injection supporta scope come:
Di seguito un esempio di dipendenze con scope con container annidati usando
dependency-injector.Nell'esempio sopra,
MyResourceimita una classe che richiede setup e teardown, come aprire/chiudere un file o un database.Il
containerfornisce la risorsa usandoproviders.Resourcee quando usiwith container.resource()entra ed esce correttamente dal contesto.Questo pattern garantisce che risorse come file handle, connessioni al database o sessioni di rete siano gestite in modo pulito.
Immagina un endpoint API che ha bisogno di una sessione di database per la richiesta corrente o di un logger con contesto specifico della richiesta.
Con le dipendenze con scope, il framework crea un container con scope di richiesta quando la richiesta inizia, risolvendo tutte le dipendenze da quello scope.
Quando la richiesta termina, eventuali cleanup necessari, come la chiusura delle sessioni di database, avvengono automaticamente. Questo assicura che gli stati non siano condivisi tra richieste ed evita perdite di risorse.
Iniezione asincrona
Per gestire in modo efficiente i task I/O-bound, hai bisogno dell'esecuzione asincrona. I sistemi di dependency injection in framework come FastAPI e
dependency-injectoroffrono l'iniezione asincrona usandoasync/await.Per iniezione asincrona si intende l'iniezione di dipendenze che sono funzioni
async defo la gestione diasync resourcescome connessioni a database asincroni, API esterne o task in background.Invece di risolverle in modo sincrono, il sistema di dependency injection attende che vengano risolte prima di iniettarle.
Di seguito un esempio di dipendenza async in FastAPI:
FastAPI gestisce automaticamente la risoluzione async di
get_usere ne inietta il risultato.dependency-injectorsupporta anche la gestione del ciclo di vita async usando i providerAsyncResource.Nel codice sopra:
resources.AsyncResourceè usato per la logica di setup/teardown asincrona.await container.init_resources()inizializza tutte le risorse e chiamainit.await container.shutdown_resources()pulisce tutte le risorse.await container.db()ottiene l'istanza reale della risorsa, cioèAsyncDB.Usare l'iniezione async aiuta a mantenere l'app performante evitando I/O sincrono in route asincrone, ed elimina la necessità di attendere manualmente le dipendenze nella tua logica.
Implicazioni di sicurezza
Sebbene la dependency injection migliori modularità e testabilità, può introdurre rischi di sicurezza se non gestita con attenzione.
Un rischio significativo è la dependency confusion, in cui pacchetti malevoli imitano quelli legittimi. Per esempio, alcuni attaccanti hanno caricato pacchetti su PyPI con nomi usati in sistemi interni aziendali per indurre le app a usare la versione malevola.
Per restare al sicuro, assicurati di non iniettare mai componenti non attendibili o controllati dall'utente e valida le fonti di configurazione, come le variabili d'ambiente, prima che vengano iniettate.
Usa file
requirements.txtopoetry.lockper bloccare le versioni esatte, e automatizza la scansione di sicurezza quando cambiano le dipendenze. Inoltre, verifica regolarmente le tue dipendenze per vulnerabilità.Centralizzare il controllo delle dipendenze è essenziale, perché assicura coerenza tra le applicazioni e facilita l'applicazione di patch di sicurezza o aggiornamenti di versione. Aiuta a evitare "dipendenze ombra" iniettate in modi diversi nella codebase.
Diversi strumenti possono rilevare rischi e migliorare la tua postura di sicurezza:
Applicazioni reali
La dependency injection non è solo un pattern teorico; viene usata in vari sistemi di produzione per gestire la complessità, migliorare la flessibilità e semplificare i test.
Configurazione dei servizi web
Nelle applicazioni web moderne, la dependency injection ha un ruolo cruciale nella gestione di servizi standard come autenticazione, logging e accesso al database.
Framework come FastAPI svolgono un ruolo essenziale nella risoluzione di route e dipendenze.
L'esempio seguente illustra un'implementazione centralizzata dell'autenticazione in FastAPI.
Nel codice sopra,
get_current_user()è iniettata come dipendenza della route, ma la logica di verifica del token è centralizzata e riutilizzabile.Puoi fare lo stesso per l'accesso al database, in modo che le sessioni siano gestite e pulite automaticamente.
Ecco un esempio di come usare la dependency injection per centralizzare l'accesso al database.
Nel codice sopra,
get_db_sessionè un provider di dipendenze. Quando viene chiamata la routeread_items, FastAPI inietta automaticamente il parametrodb, consentendo alla route di accedere al database senza gestire manualmente setup o teardown della sessione.Test-driven development
La dependency injection rende facile cambiare le dipendenze che i componenti ricevono, il che è importante in fase di test.
Puoi sostituire i servizi reali con mock durante i test, rendendo il codice più modulare e più facile da testare.
FastAPI e
dependency-injectorsupportano la sostituzione delle dipendenze in fase di test.Ecco un esempio in FastAPI.
Nell'esempio di codice sopra,
get_db_sessionè sovrascritta con un database mock durante i test; quindi non c'è bisogno di modificare il codice di produzione.Anche in
depedency-injectorpuoi sostituire il tuo database reale con un database mock per i test, come nell'esempio seguente.Il metodo
override()sostituisce la dipendenza reale con un mock, in modo pulito, controllato e reversibile.Ecco alcune strategie da usare per le override delle dipendenze:
FastAPI.dependency_overridesin FastAPI odependency_injector.override()independency-injector.pytestper creare mock riutilizzabili e iniettarli tramite fixture.Best practice e anti-pattern
Sebbene la dependency injection sia pensata per rendere il codice modulare e facile da testare, può portare a bug o creare complessità nel codice.
Pratiche raccomandate
Ecco alcune best practice da seguire quando crei dipendenze:
Errori comuni
Ecco alcune insidie comuni da evitare quando usi la dependency injection.
Testing della Dependency Injection in Python
La dependency injection rende facile iniettare oggetti mock nei componenti, semplificando così gli unit test. Ciò ti permette di separare i componenti fondamentali da quelli specifici per i test.
Con la dependency injection, i test sono deterministici, senza effetti collaterali e si eseguono più velocemente.
Ecco un esempio che mostra come usare la Dependency Injection per iniettare versioni mock, stub o fake di tali dipendenze nei test.
Nei test, sostituisci
EmailServicecon un mock:Assicurati di usare spesso la dependency injection ed evita il monkey patching per la manutenibilità e per evitare rotture quando cambiano i nomi interni.
Conclusione
Anche se potresti non aver bisogno della dependency injection inizialmente per progetti piccoli, man mano che il progetto cresce e scala, puoi beneficiare della struttura e della modularità offerte dalla dependency injection.
Guardando avanti, il futuro della dependency injection in Python è promettente, e possiamo aspettarci un'evoluzione con un sistema di tipi migliorato e una gestione del ciclo di vita async potenziata.
Padroneggiare la dependency injection è essenziale per diventare uno sviluppatore Python fluente. Ma non finisce qui: molti altri concetti avanzati possono elevare le tue skill in Python.
Per aiutarti ad accelerare il percorso di apprendimento, ecco alcune delle nostre risorse da consultare.
Corsi:
Articoli del blog: