Injection de dépendances Python : Développer un code modulaire et testable
Découvrez comment rendre votre code modulaire, testable et facile à maintenir en comprenant et en mettant en œuvre divers frameworks d'injection de dépendances Python.
L'injection de dépendances est un modèle de conception dans lequel des objets ou des services (nous entendons ici les dépendances) sont fournis à une classe depuis l'extérieur, au lieu d'être créés en interne par la classe. Cela permet de dissocier les composants.
L'utilisation de l'injection de dépendances en Python présente de nombreux avantages, tels que :
Modularité : Votre code est divisé en parties plus petites et réutilisables.
Testabilité : Il est plus facile de tester votre code, car vous pouvez remplacer les éléments réels par des éléments fictifs.
Facilité de maintenance : Vous pouvez mettre à jour ou modifier certaines parties de votre code sans perturber le reste du système.
Dans ce tutoriel, je vais vous montrer comment utiliser l'injection de dépendances en Python à travers des exemples simples et pratiques.
Comprendre l'injection de dépendances en Python
Pour comprendre comment mettre en œuvre l'injection de dépendances, il est essentiel de bien saisir les principes clés qui la régissent.
L'injection de dépendances repose sur un principe appelé « inversion de contrôle » (IoC), qui signifie qu'au lieu de créer et de gérer ses dépendances, une classe les reçoit d'une source externe. Cela permet de se concentrer sur une seule tâche et rend votre code plus clair.
L'injection de dépendances en Python comprend les méthodes courantes suivantes, bien qu'il en existe d'autres :
Injection du constructeur : Transmettez les dépendances via le constructeur de classe.
Injection du setter : Définissez les dépendances à l'aide d'une méthode de définition après la création de l'objet.
Injection de méthode : Transmettez les dépendances directement à la méthode qui en a besoin.
Pourquoi utiliser l'injection de dépendances en Python ?
En tant que développeur, la compréhension et la mise en œuvre de l'injection de dépendances peuvent faire une grande différence dans la conception de votre code. Examinons quelques avantages de l'injection de dépendances.
Modularité, réutilisabilité et flexibilité
L'injection de dépendances vous permet de décomposer votre code en composants plus petits et plus ciblés, ce qui offre une plus grande flexibilité et modularité.
Chaque partie ou composant exécute une tâche spécifique et dépend de composants externes, ce qui facilite sa réutilisation dans différentes parties de votre application ou de celle d'un autre développeur.
Par exemple, si vous avez une classe qui envoie des messages. Au lieu d'intégrer une logique d'envoi d'e-mails ou de SMS directement dans votre service, vous pouvez injecter une classe d'expéditeur d'e-mails ou de SMS.
À présent, votre classe qui envoie des messages ne se soucie pas de la manière dont le message est envoyé, mais utilise simplement l'expéditeur qui lui est fourni. Cela signifie que vous pouvez utiliser votre expéditeur de messages avec différents expéditeurs d'e-mails ou de SMS.
Maintenabilité et testabilité
Lorsque vous utilisez l'injection de dépendances, vous pouvez facilement remplacer les dépendances réelles par des données fictives pendant les tests. Cela simplifie la rédaction de tests unitaires sans dépendre d'API ou de bases de données.
Supposons, à titre d'exercice de réflexion, que vous disposiez d'une classe qui enregistre des données dans une base de données. À des fins de test, vous pouvez injecter une base de données factice à la place d'une base de données réelle afin d'éviter de lier votre test à la base de données de production.
L'injection de dépendances vous permet de modifier des parties spécifiques de votre code sans perturber l'ensemble du système. Cela est particulièrement utile lorsque vous devez changer de prestataire de paiement dans une application, par exemple passer de Stripe à PayPal, sans modifier le reste de votre code.
Inversion de contrôle et dépendances interchangeables
Le concept d'inversion de contrôle est le suivant : l'injection de dépendances transfère la responsabilité de la création et de la gestion des dépendances à une source externe.
Cela signifie que vous pouvez facilement remplacer une implémentation par une autre. Par exemple, si vous disposez d'une classe ReportGenerator qui formate et exporte des rapports.
Au lieu que la classe décide du format d'exportation, vous pourriez y injecter une classe d'exportation, telle que PDFExporter, ExcelExporter ou CSVExporter.
La classe ReportGenerator ne se soucie pas de la manière dont l'exportation s'effectue. Par conséquent, pour modifier un format, il suffit de remplacer l'exportateur que vous injectez, sans modifier l'ReportGenerator.
Mise en œuvre de l'injection de dépendances en Python
Maintenant que vous avez une idée claire de ce qu'est l'injection de dépendances et de son utilité, voyons comment vous pouvez l'implémenter en Python. Commençons par la méthode la plus simple : l'injection manuelle des dépendances.
Injection de dépendance manuelle
Dans l'injection de dépendances manuelle, vous créez vous-même les dépendances et les transmettez à la classe ou à la fonction qui en a besoin.
Cette méthode est efficace pour les petites applications, mais à mesure que votre application évolue, il est recommandé d'utiliser un framework d'injection de dépendances afin d'éviter les erreurs.
Voici un exemple de code sans injection de dépendance
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!")Propulsé par
Dans l'exemple ci-dessus, UserNotifier est étroitement lié à EmailService. Il n'est pas facile de remplacer ou de simuler l'EmailService pour effectuer des tests.
Voici une autre version, mais avec injection de dépendances manuelle.
La version ci-dessus est flexible et vous permet d'intégrer divers services de messagerie ; vous pouvez également remplacer EmailService par une simulation lors des tests.
Utilisation du framework d'injection de dépendances
À mesure que votre application se développe, la gestion des dépendances devient complexe et fastidieuse.
Container définit comment les instances sont créées
Singleton garantit qu'il n'existe qu'une seule instance d'EmailService
Factory crée une nouvelle instance d'UserNotifier avec l'EmailService injectée automatiquement.
Mécanisme de câblage
Vous pouvez utiliser des décorateurs tels que @inject pour réduire le besoin de passer manuellement les dépendances.
Voici une autre version de l'exemple précédent :
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()Propulsé par
Dans la version actuelle ;
Sans ajouter manuellement la dépendance, le décorateur d'@inject s injecte automatiquement la dépendance de fonction ou de classe.
La classe Provide indique à l'dependency-injector quelle dépendance injecter.
Cette approche élimine le besoin de dépendances de câblage manuelles depuis le conteneur.
Cela est utile dans les applications plus importantes où les services peuvent dépendre de plusieurs composants (d'où la nécessité de les initialiser automatiquement).
Utilisation de l'injection de dépendances dans les frameworks Python courants
La plupart des frameworks Python modernes facilitent l'utilisation de l'injection de dépendances, ce qui vous permet d'écrire un code plus propre et plus facile à tester.
Dans cette section, vous apprendrez comment l'injection de dépendances est appliquée dans Flask, Django et FastAPI.
Injection de dépendances dans Flask
Flask ne prend pas en charge l'injection de dépendances de manière native, mais vous pouvez l'implémenter à l'aide de la bibliothèque dependency-injector en suivant les étapes ci-dessous.
Étape 1 : Définissez vos services
classGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Propulsé par
Étape 2 : Configurez votre conteneur d'injection de dépendances.
from dependency_injector import containers, providers
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Propulsé par
Étape 3 : Veuillez créer l'application Flask et injecter les dépendances.
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)Propulsé par
À partir du code ci-dessus :
La fonction name = request.args.get(“name”, “World”) extrait l'name de la chaîne de requête et utilise par défaut ”World” si aucune valeur n'est fournie.
return greeting_service.get_greeting(name) appelle la méthode sur le service injecté et renvoie un message de bienvenue.
container.wire(modules=[__name__]) indique à l'injecteur de dépendances d'analyser ce module à la recherche de décorateurs d'@inject s et de marqueurs d'Provide[...] afin de les relier aux fournisseurs réels.
Étape 4 : Veuillez tester l'application.
Veuillez exécuter l'application sur votre terminal à l'aide de la commande « python .py », puis rendez-vous à l'adresse URL « http://localhost:5000/greet?name=Jacob ».
Vous devriez voir ce qui suit.
Injection de dépendances dans Django
Tout comme Flask, Django ne propose pas d'injection de dépendances intégrée. Cependant, vous pouvez utiliser ses vues basées sur les classes, ses middlewares et sa structure d'application pour intégrer l'injection de dépendances manuellement ou à l'aide de la bibliothèque dependency-injector.
Voici une procédure étape par étape pour implémenter l'injection de dépendances dans Django.
Étape 1 : Définissez votre service
Créez un service simple contenant votre logique métier dans un nouveau fichier services.py dans votre dossier app.
# myapp/services.pyclassGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Propulsé par
Étape 2 : Configurer le conteneur d'injection de dépendances
Dans le répertoire de votre application, veuillez créer un conteneur à l'aide de la bibliothèque dependency-injector dans un nouveau fichier containers.py.
Étape 3 : Créer une vue avec des dépendances injectées
Veuillez utiliser le décorateur @inject dans votre vue pour recevoir les dépendances dans 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)Propulsé par
Étape 4 : Mettre à jour le fichier urls.py
Dans votre dossier de projet, accédez au fichier urls.py et connectez votre injecteur de dépendances au système de routage URL de 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),]Propulsé par
Étape 5 : Connectez le conteneur dans apps.py
Veuillez vous rendre sur apps.py et connecter le conteneur à l'application.
# 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"])Propulsé par
Veuillez ensuite accéder à la configuration de votre application dans INSTALLED_APPS et ajouter votre application.
# settings.py
INSTALLED_APPS =['myapp.apps.MyAppConfig',...]Propulsé par
Étape 6 : Tester la vue
Démarrez votre serveur Django et accédez à la vue via l'URL http://localhost:8000/greet/?name=Django.
Vous devriez voir ce qui suit.
Injection de dépendances dans FastAPI
FastAPI intègre la prise en charge de l'injection de dépendances grâce au marqueur spécial ` Depends() `, qui injecte tout ce que renvoie une fonction.
Voici un guide étape par étape expliquant comment procéder à l'injection de dépendances FastAPI.
Étape 1 : Définir un service
classGreetingService:defget_greeting(self, name:str)->str:returnf"Hello, {name}!"Propulsé par
Étape 2 : Créer la fonction de dépendance
defget_greeting_service():return GreetingService()Propulsé par
Étape 3 : Configurer l'application FastAPI et utiliser l'injection de dépendances
app = FastAPI()@app.get("/greet")defgreet(
name:str="World", service: GreetingService = Depends(get_greeting_service)):return{"message": service.get_greeting(name)}Propulsé par
Étape 4 : Exécutez et testez l'application.
Veuillez démarrer votre serveur à l'aide de la commande « uvicorn main:app –reload » et rendez-vous à l'adresse URL « http://localhost:8000/greet?name=FastAPI ». Vous devriez voir la réponse suivante.
Comparaison des frameworks d'injection de dépendances Python
Il existe plusieurs frameworks de dépendances Python, chacun avec ses propres atouts, syntaxe et cas d'utilisation. Le choix du cadre approprié dépend de la taille et de la structure de votre projet.
Voici une comparaison de quelques frameworks Python courants pour l'injection de dépendances.
Fournit l'injection de dépendances pour les vues et les intergiciels.
Dépend de injector
Modèles de mise en œuvre avancés
À mesure que le code de votre application s'étoffe, la gestion des dépendances devient plus complexe.
Vous pouvez utiliser l'injection de dépendances avancée pour gérer des tâches telles que les ressources au niveau de la requête, le nettoyage et la séparation des préoccupations.
Dépendances limitées
Les dépendances limitées vous permettent de contrôler la durée de persistance d'une dépendance, par exemple lorsque vous souhaitez un nouvel objet par requête, un objet partagé pendant toute la durée de vie de l'application ou le nettoyage automatique de ressources telles que les connexions à la base de données.
La plupart des frameworks d'injection de dépendances prennent en charge des portées telles que :
Singleton : Une instance pour l'ensemble de l'application.
Usine : Une nouvelle instance est créée à chaque injection.
Local au thread ou à la requête : Une seule instance par thread/demande.
Ressource : Géré avec une logique de configuration et de démontage.
Voici un exemple de dépendances de portée avec des conteneurs imbriqués utilisant l'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")Propulsé par
Dans l'exemple ci-dessus, l'MyResource imite une classe qui nécessite une configuration et un démontage, comme l'ouverture/la fermeture d'un fichier ou d'une base de données.
L'container fournit la ressource à l'aide de providers.Resource, et lorsque vous utilisez with container.resource(), elle entre et sort correctement du contexte.
Ce modèle garantit une gestion claire des ressources telles que les descripteurs de fichiers, les connexions à la base de données ou les sessions réseau.
Imaginez un point de terminaison API qui nécessite une session de base de données pour la requête en cours ou un enregistreur avec un contexte spécifique à la requête.
Avec les dépendances limitées, le framework crée un conteneur limité à la requête lorsque la requête commence à résoudre toutes les dépendances de cette portée.
À la fin de la requête, toutes les opérations de nettoyage nécessaires, telles que la fermeture des sessions de base de données, sont effectuées automatiquement. Cela garantit que les états ne sont pas partagés entre les requêtes et évite les fuites de ressources.
Injection asynchrone
Pour gérer efficacement les tâches liées aux E/S, une exécution asynchrone est nécessaire. Les systèmes d'injection de dépendances dans des frameworks tels que FastAPI et dependency-injector offrent une injection asynchrone à l'aide de async/await.
L'injection asynchrone désigne l'injection de dépendances qui sont des fonctions d'async def, ou la gestion d'async resources telles que les connexions asynchrones à une base de données, les API externes ou les tâches en arrière-plan.
Plutôt que de les résoudre de manière synchrone, le système d'injection de dépendances attend qu'elles soient résolues avant de procéder à l'injection.
Voici un exemple de dépendance asynchrone dans 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}Propulsé par
FastAPI gère automatiquement la résolution asynchrone de l'get_user et injecte le résultat.
dependency-injector prend également en charge la gestion asynchrone du cycle de vie à l'aide de fournisseurs d'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()Propulsé par
Dans le code ci-dessus :
resources.AsyncResource est utilisé pour la logique de configuration/démontage asynchrone.
await container.init_resources() initialise toutes les ressources et appelle l'init.
await container.shutdown_resources() nettoie toutes les ressources.
await container.db() obtient l'instance de ressources réelle, qui est AsyncDB.
L'utilisation de l'injection de dépendances asynchrones contribue à maintenir les performances de votre application en évitant les E/S synchrones dans les routes asynchrones et élimine le besoin d'attendre manuellement les dépendances dans votre logique.
Conséquences en matière de sécurité
Si l'injection de dépendances améliore la modularité et la testabilité, elle peut également introduire des risques potentiels pour la sécurité si elle n'est pas gérée avec soin.
Un risque important est la confusion des dépendances, où des paquets malveillants imitent des paquets légitimes. Par exemple, des attaquants ont téléchargé des paquets sur PyPI avec des noms utilisés dans les systèmes internes des entreprises afin de tromper les applications et les inciter à utiliser la version malveillante.
Pour garantir votre sécurité, veuillez vous assurer de ne jamais injecter de composants non fiables ou contrôlés par l'utilisateur et de valider les sources de configuration, telles que les variables d'environnement, avant leur injection.
Veuillez utiliser les fichiers « requirements.txt » ou « poetry.lock » pour verrouiller des versions précises et automatiser l'analyse de sécurité lors des modifications de dépendances. De plus, veuillez vous assurer de vérifier régulièrement vos dépendances afin de détecter toute vulnérabilité.
Il est également essentiel de centraliser le contrôle de vos dépendances, car cela garantit la cohérence entre vos applications et facilite l'application des correctifs de sécurité ou des mises à jour de version. Cela permet d'éviter les « dépendances cachées » introduites de différentes manières dans le code.
Plusieurs outils permettent de détecter les risques et d'améliorer votre posture de sécurité :
Snyk: Recherche les vulnérabilités connues dans les paquets Python et propose des correctifs.
Sécurité: Vérification des paquets non sécurisés et des vulnérabilités du code.
Bandit: Analyseur de code statique pour Python axé sur les problèmes de sécurité.
Applications concrètes
L'injection de dépendances n'est pas seulement un modèle théorique ; elle est utilisée dans divers systèmes de production pour gérer la complexité, améliorer la flexibilité et rationaliser les tests.
Configuration du service Web
Dans les applications web modernes, l'injection de dépendances joue un rôle crucial dans la gestion des services standard, tels que l'authentification, la journalisation et l'accès aux bases de données.
Les frameworks tels que FastAPI jouent un rôle essentiel dans la résolution des routes et des dépendances.
L'exemple ci-dessous illustre une implémentation d'authentification centralisée dans 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']}"}Propulsé par
Dans le code ci-dessus, get_current_user() est injecté en tant que dépendance de route, mais la logique de vérification du jeton est centralisée et réutilisable.
Vous pouvez également procéder de la même manière pour l'accès à la base de données, de sorte que les sessions de base de données soient automatiquement gérées et nettoyées.
Voici un exemple illustrant comment utiliser l'injection de dépendances pour centraliser l'accès à la base de données.
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")Propulsé par
Dans le code ci-dessus, l'get_db_session est un fournisseur de dépendances. Lorsque la route read_items est appelée, FastAPI injecte automatiquement le paramètre db, permettant à la route d'accéder à la base de données sans avoir à gérer manuellement la configuration ou la fermeture de la session.
Développement piloté par les tests
L'injection de dépendances vous permet de modifier facilement les dépendances des composants, ce qui est important lors des tests.
Vous pouvez remplacer les services réels par des simulacres pendant les tests,ce qui rend votre code plus modulaire et plus facile à tester.
FastAPI et l'dependency-injector nt la possibilité de remplacer les dépendances lors des tests.
Voici un exemple dans 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 ==200Propulsé par
Dans l'exemple de code ci-dessus, la méthode ` get_db_session ` est remplacée par une base de données fictive pendant les tests ; il n'est donc pas nécessaire de modifier le code de production.
De même, dans depedency-injector, vous pouvez remplacer votre base de données réelle par une base de données fictive à des fins de test, comme dans l'exemple ci-dessous.
from containers import Container
deftest_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()assert service.get_data()=="mocked"Propulsé par
La méthode d'override() remplace la dépendance réelle par une simulation, qui est propre, contrôlée et réversible.
Voici quelques stratégies à utiliser pour remplacer les dépendances :
Injection du constructeur : Veuillez vous assurer de transmettre les mocks directement au constructeur.
Remplacement des paramètres du framework : Veuillez utiliser les outils de remplacement intégrés tels que FastAPI.dependency_overrides dans FastAPI ou dependency_injector.override() dans dependency-injector.
Calendrier : Veuillez utiliser des frameworks de test tels que pytest pour créer des simulacres réutilisables et les injecter via des fixtures.
Meilleures pratiques et anti-modèles
Bien que l'injection de dépendances vise à rendre le code modulaire et facile à tester, elle peut entraîner des bogues ou complexifier le code.
Pratiques recommandées
Voici quelques bonnes pratiques à respecter lors de la création de dépendances :
Programmez des interfaces, pas des implémentations : Définissez vos dépendances à l'aide de classes de base abstraites ou d'interfaces plutôt que d'implémentations complexes. Cela facilite le test de votre code et encourage également l'utilisation de simulacres dans les tests unitaires.
Privilégiez la composition à l'héritage : Plutôt que de créer des hiérarchies de classes complexes, composez des objets à l'aide de dépendances injectées afin de conserver des composants compacts et ciblés.
Configuration centralisée des dépendances : Utilisez un seul conteneur ou module pour définir et gérer les dépendances, ce qui facilite la substitution et les tests.
Évitez les dépendances circulaires : Concevez soigneusement votre graphe de dépendances afin d'éviter les références circulaires en utilisant des fonctions d'usine ou des fournisseurs pour retarder l'instanciation.
Erreurs courantes
Voici quelques pièges courants à éviter lors de l'utilisation de l'injection de dépendances.
Anti-modèle du localisateur de services : Évitez d'utiliser un conteneur global pour récupérer dynamiquement des dépendances, car cela peut masquer des dépendances et rendre le code difficile à comprendre et à tester.
Surcharge de gaz : Évitez d'injecter trop de services dans une seule classe ou fonction afin d'éviter que votre code ne donne lieu à des constructeurs trop lourds et à une logique difficile à tester.
Mauvaise gestion du champ d'application et fuites au niveau de l'État : Veuillez vous assurer de toujours utiliser la portée appropriée afin d'éviter le partage d'état entre les utilisateurs ou les tests.
Test d'injection de dépendances en Python
L'injection de dépendances facilite l'injection d'objets fictifs dans vos composants, simplifiant ainsi les tests unitaires. Cela vous permet de séparer les composants fondamentaux de ceux spécifiques aux tests.
Grâce à l'injection de dépendances, les tests sont déterministes, n'ont aucun effet secondaire et s'exécutent plus rapidement.
Voici un exemple illustrant comment utiliser l'injection de dépendances pour injecter des versions fictives, simulées ou factices de ces dépendances dans vos tests.
Dans les tests, veuillez remplacer EmailService par une simulation :
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]"Propulsé par
Veuillez vous assurer d'utiliser fréquemment l'injection de dépendances et d'éviter les modifications arbitraires afin de faciliter la maintenance et d'éviter des changements importants en cas de modification des noms internes.
Conclusion
Même si vous n'avez pas besoin de l'injection de dépendances au début pour les petits projets, vous pourrez bénéficier de la structure et de la modularité offertes par l'injection de dépendances à mesure que le projet prendra de l'ampleur et évoluera.
À l'avenir, l'injection de dépendances dans Python semble prometteuse, et nous pouvons nous attendre à ce qu'elle se poursuive avec un système de types amélioré et une gestion du cycle de vie asynchrone optimisée.
La maîtrise de l'injection de dépendances est essentielle pour devenir un développeur Python expérimenté. Cependant, cela ne s'arrête pas là ; de nombreux autres concepts avancés peuvent améliorer vos compétences en Python.
Pour vous aider à accélérer votre apprentissage, voici quelques-unes de nos ressources que nous vous recommandons de consulter.
Instructeur expérimenté en science des données et biostatisticien avec une expertise en Python, R et apprentissage automatique.
Foire aux questions
Qu'est-ce que l'injection de dépendances ?⌄
L'injection de dépendances est un modèle de conception qui vous permet d'écrire un code plus propre en créant des dépendances au sein d'une classe, qui peuvent être transmises depuis l'extérieur, ce qui facilite la gestion et la modification de votre code.
Quels sont les principaux avantages de l'injection de dépendances ?⌄
L'injection de dépendances rend votre code modulaire, testable et facile à maintenir.
Comment puis-je implémenter l'injection de dépendances en Python ?⌄
Vous pouvez implémenter l'injection de dépendances en Python manuellement ou à l'aide de bibliothèques telles que dependency_injector.
Qu'est-ce que l'injection asynchrone ?⌄
L'injection asynchrone désigne l'injection de dépendances qui sont des fonctions d'async def, ou la gestion d'async resources telles que les connexions asynchrones à une base de données, les API externes ou les tâches en arrière-plan.
Quels sont les risques de sécurité liés à l'injection de dépendances et comment puis-je les gérer ?⌄
Un risque important est la confusion des dépendances, où des paquets malveillants imitent des paquets légitimes. Pour éviter cela, vous pouvez utiliser des outils tels que Snyk, Xygeni, Safety ou Bandit afin d'analyser votre code et vos paquets à la recherche de vulnérabilités.
Découvrez les classes abstraites Python, leur utilité et comment utiliser le module `abc` pour garantir la cohérence des interfaces. Comprend des exemples pratiques et des bonnes pratiques pour une mise en œuvre efficace.
Apprenez à utiliser JSON en Python, notamment la sérialisation, la désérialisation, le formatage, l'optimisation des performances, la gestion des API, ainsi que les limites et les alternatives de JSON.
Tutoriel complet d'introduction aux boucles Python. Apprenez et pratiquez les boucles while et for, les boucles imbriquées, les mots-clés break et continue, la fonction range et bien plus encore.
L'injection de dépendances est un modèle de conception dans lequel des objets ou des services (nous entendons ici les dépendances) sont fournis à une classe depuis l'extérieur, au lieu d'être créés en interne par la classe. Cela permet de dissocier les composants.
L'utilisation de l'injection de dépendances en Python présente de nombreux avantages, tels que :
Dans ce tutoriel, je vais vous montrer comment utiliser l'injection de dépendances en Python à travers des exemples simples et pratiques.
Comprendre l'injection de dépendances en Python
Pour comprendre comment mettre en œuvre l'injection de dépendances, il est essentiel de bien saisir les principes clés qui la régissent.
L'injection de dépendances repose sur un principe appelé « inversion de contrôle » (IoC), qui signifie qu'au lieu de créer et de gérer ses dépendances, une classe les reçoit d'une source externe. Cela permet de se concentrer sur une seule tâche et rend votre code plus clair.
L'injection de dépendances en Python comprend les méthodes courantes suivantes, bien qu'il en existe d'autres :
Pourquoi utiliser l'injection de dépendances en Python ?
En tant que développeur, la compréhension et la mise en œuvre de l'injection de dépendances peuvent faire une grande différence dans la conception de votre code. Examinons quelques avantages de l'injection de dépendances.
Modularité, réutilisabilité et flexibilité
L'injection de dépendances vous permet de décomposer votre code en composants plus petits et plus ciblés, ce qui offre une plus grande flexibilité et modularité.
Chaque partie ou composant exécute une tâche spécifique et dépend de composants externes, ce qui facilite sa réutilisation dans différentes parties de votre application ou de celle d'un autre développeur.
Par exemple, si vous avez une classe qui envoie des messages. Au lieu d'intégrer une logique d'envoi d'e-mails ou de SMS directement dans votre service, vous pouvez injecter une classe d'expéditeur d'e-mails ou de SMS.
À présent, votre classe qui envoie des messages ne se soucie pas de la manière dont le message est envoyé, mais utilise simplement l'expéditeur qui lui est fourni. Cela signifie que vous pouvez utiliser votre expéditeur de messages avec différents expéditeurs d'e-mails ou de SMS.
Maintenabilité et testabilité
Lorsque vous utilisez l'injection de dépendances, vous pouvez facilement remplacer les dépendances réelles par des données fictives pendant les tests. Cela simplifie la rédaction de tests unitaires sans dépendre d'API ou de bases de données.
Supposons, à titre d'exercice de réflexion, que vous disposiez d'une classe qui enregistre des données dans une base de données. À des fins de test, vous pouvez injecter une base de données factice à la place d'une base de données réelle afin d'éviter de lier votre test à la base de données de production.
L'injection de dépendances vous permet de modifier des parties spécifiques de votre code sans perturber l'ensemble du système. Cela est particulièrement utile lorsque vous devez changer de prestataire de paiement dans une application, par exemple passer de Stripe à PayPal, sans modifier le reste de votre code.
Inversion de contrôle et dépendances interchangeables
Le concept d'inversion de contrôle est le suivant : l'injection de dépendances transfère la responsabilité de la création et de la gestion des dépendances à une source externe.
Cela signifie que vous pouvez facilement remplacer une implémentation par une autre. Par exemple, si vous disposez d'une classe
ReportGeneratorqui formate et exporte des rapports.Au lieu que la classe décide du format d'exportation, vous pourriez y injecter une classe d'exportation, telle que
PDFExporter,ExcelExporterouCSVExporter.La classe
ReportGeneratorne se soucie pas de la manière dont l'exportation s'effectue. Par conséquent, pour modifier un format, il suffit de remplacer l'exportateur que vous injectez, sans modifier l'ReportGenerator.Mise en œuvre de l'injection de dépendances en Python
Maintenant que vous avez une idée claire de ce qu'est l'injection de dépendances et de son utilité, voyons comment vous pouvez l'implémenter en Python. Commençons par la méthode la plus simple : l'injection manuelle des dépendances.
Injection de dépendance manuelle
Dans l'injection de dépendances manuelle, vous créez vous-même les dépendances et les transmettez à la classe ou à la fonction qui en a besoin.
Cette méthode est efficace pour les petites applications, mais à mesure que votre application évolue, il est recommandé d'utiliser un framework d'injection de dépendances afin d'éviter les erreurs.
Voici un exemple de code sans injection de dépendance
Dans l'exemple ci-dessus,
UserNotifierest étroitement lié àEmailService. Il n'est pas facile de remplacer ou de simuler l'EmailServicepour effectuer des tests.Voici une autre version, mais avec injection de dépendances manuelle.
La version ci-dessus est flexible et vous permet d'intégrer divers services de messagerie ; vous pouvez également remplacer
EmailServicepar une simulation lors des tests.Utilisation du framework d'injection de dépendances
À mesure que votre application se développe, la gestion des dépendances devient complexe et fastidieuse.
Le cadre
dependency-injectorcadre offre une approche structurée.Architecture de fournisseur de conteneurs
Le framework d'
dependency-injectors fonctionne selon une architecture conteneur-fournisseur.Dans l'exemple ci-dessus :
Containerdéfinit comment les instances sont crééesSingletongarantit qu'il n'existe qu'une seule instance d'EmailServiceFactorycrée une nouvelle instance d'UserNotifieravec l'EmailServiceinjectée automatiquement.Mécanisme de câblage
Vous pouvez utiliser des décorateurs tels que
@injectpour réduire le besoin de passer manuellement les dépendances.Voici une autre version de l'exemple précédent :
Dans la version actuelle ;
Sans ajouter manuellement la dépendance, le décorateur d'
@injects injecte automatiquement la dépendance de fonction ou de classe.La classe
Provideindique à l'dependency-injectorquelle dépendance injecter.Cette approche élimine le besoin de dépendances de câblage manuelles depuis le conteneur.
Cela est utile dans les applications plus importantes où les services peuvent dépendre de plusieurs composants (d'où la nécessité de les initialiser automatiquement).
Utilisation de l'injection de dépendances dans les frameworks Python courants
La plupart des frameworks Python modernes facilitent l'utilisation de l'injection de dépendances, ce qui vous permet d'écrire un code plus propre et plus facile à tester.
Dans cette section, vous apprendrez comment l'injection de dépendances est appliquée dans Flask, Django et FastAPI.
Injection de dépendances dans Flask
Flask ne prend pas en charge l'injection de dépendances de manière native, mais vous pouvez l'implémenter à l'aide de la bibliothèque
dependency-injectoren suivant les étapes ci-dessous.Étape 1 : Définissez vos services
Étape 2 : Configurez votre conteneur d'injection de dépendances.
Étape 3 : Veuillez créer l'application Flask et injecter les dépendances.
À partir du code ci-dessus :
La fonction
name = request.args.get(“name”, “World”)extrait l'namede la chaîne de requête et utilise par défaut”World”si aucune valeur n'est fournie.return greeting_service.get_greeting(name)appelle la méthode sur le service injecté et renvoie un message de bienvenue.container.wire(modules=[__name__])indique à l'injecteur de dépendances d'analyser ce module à la recherche de décorateurs d'@injects et de marqueurs d'Provide[...]afin de les relier aux fournisseurs réels.Étape 4 : Veuillez tester l'application.
Veuillez exécuter l'application sur votre terminal à l'aide de la commande «
python .py», puis rendez-vous à l'adresse URL «http://localhost:5000/greet?name=Jacob».Vous devriez voir ce qui suit.
Injection de dépendances dans Django
Tout comme Flask, Django ne propose pas d'injection de dépendances intégrée. Cependant, vous pouvez utiliser ses vues basées sur les classes, ses middlewares et sa structure d'application pour intégrer l'injection de dépendances manuellement ou à l'aide de la bibliothèque
dependency-injector.Voici une procédure étape par étape pour implémenter l'injection de dépendances dans Django.
Étape 1 : Définissez votre service
Créez un service simple contenant votre logique métier dans un nouveau fichier
services.pydans votre dossier app.Étape 2 : Configurer le conteneur d'injection de dépendances
Dans le répertoire de votre application, veuillez créer un conteneur à l'aide de la bibliothèque
dependency-injectordans un nouveau fichiercontainers.py.Étape 3 : Créer une vue avec des dépendances injectées
Veuillez utiliser le décorateur
@injectdans votre vue pour recevoir les dépendances dansviews.py.Étape 4 : Mettre à jour le fichier urls.py
Dans votre dossier de projet, accédez au fichier
urls.pyet connectez votre injecteur de dépendances au système de routage URL de Django.Étape 5 : Connectez le conteneur dans apps.py
Veuillez vous rendre sur
apps.pyet connecter le conteneur à l'application.Veuillez ensuite accéder à la configuration de votre application dans
INSTALLED_APPSet ajouter votre application.Étape 6 : Tester la vue
Démarrez votre serveur Django et accédez à la vue via l'URL
http://localhost:8000/greet/?name=Django.Vous devriez voir ce qui suit.
Injection de dépendances dans FastAPI
FastAPI intègre la prise en charge de l'injection de dépendances grâce au marqueur spécial `
Depends()`, qui injecte tout ce que renvoie une fonction.Voici un guide étape par étape expliquant comment procéder à l'injection de dépendances FastAPI.
Étape 1 : Définir un service
Étape 2 : Créer la fonction de dépendance
Étape 3 : Configurer l'application FastAPI et utiliser l'injection de dépendances
Étape 4 : Exécutez et testez l'application.
Veuillez démarrer votre serveur à l'aide de la commande «
uvicorn main:app –reload» et rendez-vous à l'adresse URL «http://localhost:8000/greet?name=FastAPI». Vous devriez voir la réponse suivante.Comparaison des frameworks d'injection de dépendances Python
Il existe plusieurs frameworks de dépendances Python, chacun avec ses propres atouts, syntaxe et cas d'utilisation. Le choix du cadre approprié dépend de la taille et de la structure de votre projet.
Voici une comparaison de quelques frameworks Python courants pour l'injection de dépendances.
Cadre
Cordialement, Pour
Points forts
Restrictions
dependency-injectorApplications complexes dans la production
Complet, rapide et configurable
Verbeux
injectorSimple et adapté aux applications minimales
Syntaxe claire
Moins avancé que les autres.
pinjectLogique de liaison avancée
Câblage automatique
Ralentissement dû à l'introspection
punqApplications simples
Minimaliste et rapide
Fonctionnalités limitées par rapport à d'autres produits
FastAPI
Depends()Applications FastAPI
Prise en charge intégrée de l'asynchronisme et possibilité de test.
Non réutilisable en dehors de FastAPI
Flask-injectorApplications Flask
Intégration aisée avec les applications Flask
Dépend de
injectordjango-injectorProjets Django
Fournit l'injection de dépendances pour les vues et les intergiciels.
Dépend de
injectorModèles de mise en œuvre avancés
À mesure que le code de votre application s'étoffe, la gestion des dépendances devient plus complexe.
Vous pouvez utiliser l'injection de dépendances avancée pour gérer des tâches telles que les ressources au niveau de la requête, le nettoyage et la séparation des préoccupations.
Dépendances limitées
Les dépendances limitées vous permettent de contrôler la durée de persistance d'une dépendance, par exemple lorsque vous souhaitez un nouvel objet par requête, un objet partagé pendant toute la durée de vie de l'application ou le nettoyage automatique de ressources telles que les connexions à la base de données.
La plupart des frameworks d'injection de dépendances prennent en charge des portées telles que :
Voici un exemple de dépendances de portée avec des conteneurs imbriqués utilisant l'
dependency-injector.Dans l'exemple ci-dessus, l'
MyResourceimite une classe qui nécessite une configuration et un démontage, comme l'ouverture/la fermeture d'un fichier ou d'une base de données.L'
containerfournit la ressource à l'aide deproviders.Resource, et lorsque vous utilisezwith container.resource(), elle entre et sort correctement du contexte.Ce modèle garantit une gestion claire des ressources telles que les descripteurs de fichiers, les connexions à la base de données ou les sessions réseau.
Imaginez un point de terminaison API qui nécessite une session de base de données pour la requête en cours ou un enregistreur avec un contexte spécifique à la requête.
Avec les dépendances limitées, le framework crée un conteneur limité à la requête lorsque la requête commence à résoudre toutes les dépendances de cette portée.
À la fin de la requête, toutes les opérations de nettoyage nécessaires, telles que la fermeture des sessions de base de données, sont effectuées automatiquement. Cela garantit que les états ne sont pas partagés entre les requêtes et évite les fuites de ressources.
Injection asynchrone
Pour gérer efficacement les tâches liées aux E/S, une exécution asynchrone est nécessaire. Les systèmes d'injection de dépendances dans des frameworks tels que FastAPI et
dependency-injectoroffrent une injection asynchrone à l'aide deasync/await.L'injection asynchrone désigne l'injection de dépendances qui sont des fonctions d'
async def, ou la gestion d'async resourcestelles que les connexions asynchrones à une base de données, les API externes ou les tâches en arrière-plan.Plutôt que de les résoudre de manière synchrone, le système d'injection de dépendances attend qu'elles soient résolues avant de procéder à l'injection.
Voici un exemple de dépendance asynchrone dans FastAPI :
FastAPI gère automatiquement la résolution asynchrone de l'
get_useret injecte le résultat.dependency-injectorprend également en charge la gestion asynchrone du cycle de vie à l'aide de fournisseurs d'AsyncResource.Dans le code ci-dessus :
resources.AsyncResourceest utilisé pour la logique de configuration/démontage asynchrone.await container.init_resources()initialise toutes les ressources et appelle l'init.await container.shutdown_resources()nettoie toutes les ressources.await container.db()obtient l'instance de ressources réelle, qui estAsyncDB.L'utilisation de l'injection de dépendances asynchrones contribue à maintenir les performances de votre application en évitant les E/S synchrones dans les routes asynchrones et élimine le besoin d'attendre manuellement les dépendances dans votre logique.
Conséquences en matière de sécurité
Si l'injection de dépendances améliore la modularité et la testabilité, elle peut également introduire des risques potentiels pour la sécurité si elle n'est pas gérée avec soin.
Un risque important est la confusion des dépendances, où des paquets malveillants imitent des paquets légitimes. Par exemple, des attaquants ont téléchargé des paquets sur PyPI avec des noms utilisés dans les systèmes internes des entreprises afin de tromper les applications et les inciter à utiliser la version malveillante.
Pour garantir votre sécurité, veuillez vous assurer de ne jamais injecter de composants non fiables ou contrôlés par l'utilisateur et de valider les sources de configuration, telles que les variables d'environnement, avant leur injection.
Veuillez utiliser les fichiers «
requirements.txt» ou «poetry.lock» pour verrouiller des versions précises et automatiser l'analyse de sécurité lors des modifications de dépendances. De plus, veuillez vous assurer de vérifier régulièrement vos dépendances afin de détecter toute vulnérabilité.Il est également essentiel de centraliser le contrôle de vos dépendances, car cela garantit la cohérence entre vos applications et facilite l'application des correctifs de sécurité ou des mises à jour de version. Cela permet d'éviter les « dépendances cachées » introduites de différentes manières dans le code.
Plusieurs outils permettent de détecter les risques et d'améliorer votre posture de sécurité :
Applications concrètes
L'injection de dépendances n'est pas seulement un modèle théorique ; elle est utilisée dans divers systèmes de production pour gérer la complexité, améliorer la flexibilité et rationaliser les tests.
Configuration du service Web
Dans les applications web modernes, l'injection de dépendances joue un rôle crucial dans la gestion des services standard, tels que l'authentification, la journalisation et l'accès aux bases de données.
Les frameworks tels que FastAPI jouent un rôle essentiel dans la résolution des routes et des dépendances.
L'exemple ci-dessous illustre une implémentation d'authentification centralisée dans FastAPI.
Dans le code ci-dessus,
get_current_user()est injecté en tant que dépendance de route, mais la logique de vérification du jeton est centralisée et réutilisable.Vous pouvez également procéder de la même manière pour l'accès à la base de données, de sorte que les sessions de base de données soient automatiquement gérées et nettoyées.
Voici un exemple illustrant comment utiliser l'injection de dépendances pour centraliser l'accès à la base de données.
Dans le code ci-dessus, l'
get_db_sessionest un fournisseur de dépendances. Lorsque la routeread_itemsest appelée, FastAPI injecte automatiquement le paramètredb, permettant à la route d'accéder à la base de données sans avoir à gérer manuellement la configuration ou la fermeture de la session.Développement piloté par les tests
L'injection de dépendances vous permet de modifier facilement les dépendances des composants, ce qui est important lors des tests.
Vous pouvez remplacer les services réels par des simulacres pendant les tests,ce qui rend votre code plus modulaire et plus facile à tester.
FastAPI et l'
dependency-injectornt la possibilité de remplacer les dépendances lors des tests.Voici un exemple dans FastAPI.
Dans l'exemple de code ci-dessus, la méthode `
get_db_session` est remplacée par une base de données fictive pendant les tests ; il n'est donc pas nécessaire de modifier le code de production.De même, dans
depedency-injector, vous pouvez remplacer votre base de données réelle par une base de données fictive à des fins de test, comme dans l'exemple ci-dessous.La méthode d'
override()remplace la dépendance réelle par une simulation, qui est propre, contrôlée et réversible.Voici quelques stratégies à utiliser pour remplacer les dépendances :
FastAPI.dependency_overridesdans FastAPI oudependency_injector.override()dansdependency-injector.pytestpour créer des simulacres réutilisables et les injecter via des fixtures.Meilleures pratiques et anti-modèles
Bien que l'injection de dépendances vise à rendre le code modulaire et facile à tester, elle peut entraîner des bogues ou complexifier le code.
Pratiques recommandées
Voici quelques bonnes pratiques à respecter lors de la création de dépendances :
Erreurs courantes
Voici quelques pièges courants à éviter lors de l'utilisation de l'injection de dépendances.
Test d'injection de dépendances en Python
L'injection de dépendances facilite l'injection d'objets fictifs dans vos composants, simplifiant ainsi les tests unitaires. Cela vous permet de séparer les composants fondamentaux de ceux spécifiques aux tests.
Grâce à l'injection de dépendances, les tests sont déterministes, n'ont aucun effet secondaire et s'exécutent plus rapidement.
Voici un exemple illustrant comment utiliser l'injection de dépendances pour injecter des versions fictives, simulées ou factices de ces dépendances dans vos tests.
Dans les tests, veuillez remplacer
EmailServicepar une simulation :Veuillez vous assurer d'utiliser fréquemment l'injection de dépendances et d'éviter les modifications arbitraires afin de faciliter la maintenance et d'éviter des changements importants en cas de modification des noms internes.
Conclusion
Même si vous n'avez pas besoin de l'injection de dépendances au début pour les petits projets, vous pourrez bénéficier de la structure et de la modularité offertes par l'injection de dépendances à mesure que le projet prendra de l'ampleur et évoluera.
À l'avenir, l'injection de dépendances dans Python semble prometteuse, et nous pouvons nous attendre à ce qu'elle se poursuive avec un système de types amélioré et une gestion du cycle de vie asynchrone optimisée.
La maîtrise de l'injection de dépendances est essentielle pour devenir un développeur Python expérimenté. Cependant, cela ne s'arrête pas là ; de nombreux autres concepts avancés peuvent améliorer vos compétences en Python.
Pour vous aider à accélérer votre apprentissage, voici quelques-unes de nos ressources que nous vous recommandons de consulter.
Cours :
Articles de blog :