Inyección de dependencias en Python: Crea código modular y comprobable
Aprende a crear código modular, comprobable y fácil de mantener mediante la comprensión y la implementación de varios marcos de inyección de dependencias de Python.
La inyección de dependencias es un patrón de diseño en el que los objetos o servicios (nos referimos a las dependencias) se proporcionan a una clase desde el exterior, en lugar de que la clase los cree internamente. Esto desacopla los componentes.
El uso de la inyección de dependencias en Python tiene muchas ventajas, tales como:
Modularidad: Tu código se divide en partes más pequeñas y reutilizables.
Capacidad de prueba: Es más fácil probar tu código porque puedes sustituir la parte real por otras simuladas.
Mantenibilidad: Puedes actualizar o modificar partes de tu código sin afectar al resto del sistema.
En este tutorial, te mostraré cómo utilizar la inyección de dependencias en Python a través de ejemplos sencillos y prácticos.
Comprender la inyección de dependencias en Python
Para comprender cómo implementar la inyección de dependencias, es esencial que comprendas los principios clave que la rigen.
La inyección de dependencias se basa en algo llamado principio de inversión de control (IoC), que significa que, en lugar de que una clase cree y gestione sus dependencias, las recibe de una fuente externa. Esto ayuda a centrarse en una sola tarea y hace que tu código sea más limpio.
La inyección de dependencias en Python incluye estas formas comunes, aunque hay otras:
Inyección del constructor: Pasa las dependencias a través del constructor de la clase.
Inyección del setter: Establece las dependencias utilizando un método setter después de crear el objeto.
Inyección de método: Pasa las dependencias directamente al método que las necesita.
¿Por qué utilizar la inyección de dependencias en Python?
Como programador, comprender e implementar la inyección de dependencias puede marcar una gran diferencia en el diseño de tu código. Veamos algunas ventajas de la inyección de dependencias.
Modularidad, reutilización y flexibilidad
La inyección de dependencias te permite dividir el código en componentes más pequeños y específicos, lo que proporciona una mayor flexibilidad y modularidad.
Cada parte o componente se encarga de una tarea específica y depende de elementos externos, lo que facilita su reutilización en diferentes partes de tu aplicación o de la de otra persona.
Por ejemplo, si tienes una clase que envía mensajes. En lugar de integrar una lógica de correo electrónico o SMS directamente en tu servicio, puedes insertar una clase de remitente de correo electrónico o SMS.
Ahora, tu clase que envía mensajes no se preocupa por cómo se envía el mensaje, sino que simplemente utiliza el remitente que se le proporciona. Esto implica que puedes utilizar tu remitente de mensajes con diferentes remitentes de correo electrónico o SMS.
Mantenibilidad y capacidad de prueba
Al utilizar la inyección de dependencias, puedes sustituir fácilmente las dependencias reales por datos simulados durante las pruebas. Esto simplifica la escritura de pruebas unitarias sin depender de API o bases de datos.
Supongamos, como ejercicio de reflexión, que tienes una clase que guarda datos en una base de datos. A efectos de prueba, puedes inyectar una base de datos ficticia en lugar de una real para evitar que la prueba quede vinculada a la base de datos de producción.
La inyección de dependencias te permite modificar partes específicas de tu código sin romper todo el sistema. Esto resulta especialmente útil en situaciones en las que necesitas cambiar de proveedor de pagos en una aplicación, por ejemplo, de Stripe a PayPal, sin modificar el resto del código.
Inversión de control y dependencias intercambiables
La idea de la inversión de control es la siguiente: la inyección de dependencias trasladará la responsabilidad de crear y gestionar las dependencias a una fuente externa.
Esto implica que puedes cambiar fácilmente una implementación por otra; por ejemplo, si tienes una clase ReportGenerator que formatea y exporta informes.
En lugar de que la clase decida el formato de exportación, podrías insertar una clase exportadora, como PDFExporter, ExcelExporter o CSVExporter.
A la clase ` ReportGenerator ` no le importa cómo se realiza la exportación, por lo que para cambiar un formato, solo tienes que cambiar el exportador que inyectas, sin modificar en absoluto el ` ReportGenerator `.
Implementación de la inyección de dependencias en Python
Ahora que tienes una idea clara de qué es la inyección de dependencias y por qué es útil, veamos cómo puedes implementarla en Python. Comencemos con el método más sencillo: la inyección manual de dependencias.
Inyección manual de dependencias
En la inyección manual de dependencias, tú mismo creas las dependencias y las pasas a la clase o función que las necesita.
Este método funciona bien para aplicaciones pequeñas, pero a medida que tu aplicación crece, es necesario utilizar un marco de inyección de dependencias para evitar errores.
Aquí tienes un ejemplo de código sin inyección de dependencias.
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!")Con la tecnología de
En el ejemplo anterior, UserNotifier está estrechamente vinculado a EmailService. No es fácil intercambiar o simular EmailService para realizar pruebas.
Aquí hay otra versión, pero con inyección de dependencias manual.
classEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self, email_service):
self.email_service = email_service # Dependency is injecteddefnotify(self, message):
self.email_service.send_email(message)
email_service = EmailService()
notifier = UserNotifier(email_service)
notifier.notify("Welcome!")Con la tecnología de
La versión anterior es flexible y te permite inyectar varios servicios de mensajería; también puedes sustituir EmailService por una simulación en las pruebas.
Uso del marco de inyección de dependencias
A medida que tu aplicación crece, gestionar las dependencias se vuelve complejo y complicado.
Singleton garantiza que solo exista una instancia de EmailService
Factory crea un nuevo objeto de control de navegación ( UserNotifier ) con el control de navegación de la página principal ( EmailService ) inyectado automáticamente.
Mecanismo de cableado
Puedes utilizar decoradores como @inject para reducir la necesidad de pasar dependencias manualmente.
Aquí hay otra versión del ejemplo anterior:
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()Con la tecnología de
En la versión actual;
Sin añadir manualmente la dependencia, el decorador « @inject » inyecta automáticamente la dependencia de la función o clase.
La clase « Provide » indica a « dependency-injector » qué dependencia debe inyectar.
Este enfoque elimina la necesidad de dependencias de cableado manual desde el contenedor.
Es útil en aplicaciones más grandes, donde los servicios pueden depender de varios componentes (de ahí la necesidad de inicializarlos automáticamente).
Uso de la inyección de dependencias en marcos populares de Python
La mayoría de los frameworks modernos de Python facilitan el trabajo con la inyección de dependencias, lo que te permite escribir código más limpio y fácil de probar.
En esta sección, aprenderás cómo se aplica la inyección de dependencias en Flask, Django y FastAPI.
Inyección de dependencias en Flask
Flask no tiene soporte integrado para la inyección de dependencias, pero puedes implementarla utilizando la biblioteca dependency-injector siguiendo estos pasos.
Paso 1: Define tus servicios
classGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Con la tecnología de
Paso 2: Configura tu contenedor de inyección de dependencias
from dependency_injector import containers, providers
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Con la tecnología de
Paso 3: Crea la aplicación Flask e inyecta las dependencias.
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)Con la tecnología de
Del código anterior:
La función name = request.args.get(“name”, “World”) extrae name de la cadena de consulta y, si no se proporciona, utiliza ”World” como valor predeterminado.
return greeting_service.get_greeting(name) llama al método en el servicio inyectado y devuelve un mensaje de saludo.
container.wire(modules=[__name__]) indica al inyector de dependencias que busque en este módulo decoradores de inyección de dependencias ( @inject ) y marcadores de inyección de dependencias ( Provide[...] ) para conectarlos a los proveedores reales.
Paso 4: Prueba la aplicación
Ejecuta la aplicación en tu terminal utilizando el comando python .py y visita la URL http://localhost:5000/greet?name=Jacob.
Deberías ver lo siguiente.
Inyección de dependencias en Django
Al igual que Flask, Django no ofrece inyección de dependencias integrada. Sin embargo, puedes utilizar sus vistas basadas en clases, middleware y estructura de aplicaciones para integrar la inyección de dependencias manualmente o utilizando la biblioteca dependency-injector.
A continuación se muestra un procedimiento paso a paso para implementar la inyección de dependencias en Django.
Paso 1: Define tu servicio
Crea un servicio sencillo que contenga la lógica de tu negocio en un nuevo archivo services.py dentro de la carpeta de tu aplicación.
# myapp/services.pyclassGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Con la tecnología de
Paso 2: Configurar el contenedor del inyector de dependencias
Dentro del directorio de la aplicación, crea un contenedor utilizando la biblioteca dependency-injector dentro de un nuevo archivo containers.py.
# myapp/containers.pyfrom dependency_injector import containers, providers
from.services import GreetingService
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Con la tecnología de
Paso 3: Crear una vista con dependencias inyectadas
Utiliza el decorador « @inject » en tu vista para recibir las dependencias en « 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)Con la tecnología de
Paso 4: Actualiza urls.py
En la carpeta del proyecto, ve al archivo urls.py y conecta el inyector de dependencias al sistema de enrutamiento de 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),]Con la tecnología de
Paso 5: Conecta el contenedor en apps.py
Ve a apps.py y conecta el contenedor a la aplicación.
# 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"])Con la tecnología de
A continuación, ve a la configuración de la aplicación en INSTALLED_APPS y añade tu aplicación.
# settings.py
INSTALLED_APPS =['myapp.apps.MyAppConfig',...]Con la tecnología de
Paso 6: Probar la vista
Inicia tu servidor Django y accede a la vista a través de la URL http://localhost:8000/greet/?name=Django.
Deberías ver lo siguiente.
Inyección de dependencias en FastAPI
FastAPI tiene soporte integrado para la inyección de dependencias a través del marcador especial Depends(), que inyecta cualquier cosa que devuelva una función.
A continuación, se incluye una guía paso a paso sobre cómo realizar la inyección de dependencias en FastAPI.
Paso 1: Define un servicio
classGreetingService:defget_greeting(self, name:str)->str:returnf"Hello, {name}!"Con la tecnología de
Paso 3: Configurar la aplicación FastAPI y utilizar la inyección de dependencias
app = FastAPI()@app.get("/greet")defgreet(
name:str="World", service: GreetingService = Depends(get_greeting_service)):return{"message": service.get_greeting(name)}Con la tecnología de
Paso 4: Ejecuta y prueba la aplicación.
Inicia tu servidor con el comando uvicorn main:app –reload y visita la URL http://localhost:8000/greet?name=FastAPI. Deberías ver la siguiente respuesta.
Comparación de marcos de inyección de dependencias en Python
Existen varios marcos de dependencias de Python, cada uno con sus propias ventajas, sintaxis y casos de uso. La elección del marco adecuado depende del tamaño y la estructura de tu proyecto.
A continuación se muestra una comparación de algunos marcos comunes de inyección de dependencias en Python.
Proporciona inyección de dependencias para vistas y middleware.
Depende de injector
Patrones de implementación avanzados
A medida que el código base de tu aplicación se vuelve más grande, la inyección de dependencias se vuelve difícil de mantener.
Puedes utilizar la inyección de dependencias avanzada para gestionar tareas como los recursos del ámbito de la solicitud, la limpieza y la separación de preocupaciones.
Dependencias con ámbito
Las dependencias con ámbito te permiten controlar cuánto tiempo persiste una dependencia, por ejemplo, cuando deseas un nuevo objeto por solicitud, un objeto compartido durante toda la vida útil de la aplicación o la limpieza automática de recursos como las conexiones a bases de datos.
La mayoría de los marcos de inyección de dependencias admiten ámbitos como:
Singleton: Una instancia para toda la aplicación.
Fábrica: Una nueva instancia cada vez que se inyecta.
Local en el hilo o en la solicitud: Una sola instancia por hilo/solicitud.
Recurso: Gestionado con lógica de configuración y desmontaje.
A continuación se muestra un ejemplo de dependencias con ámbito y contenedores anidados utilizando 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")Con la tecnología de
En el ejemplo anterior, MyResource imita una clase que requiere configuración y desmontaje, como abrir/cerrar un archivo o una base de datos.
El controlador de recursos de clase única ( container ) proporciona el recurso mediante providers.Resource y, cuando utilizas with container.resource(), entra y sale del contexto correctamente.
Este patrón garantiza que los recursos, como los manejadores de archivos, las conexiones a bases de datos o las sesiones de red, se gestionen de forma limpia.
Imagina un punto final de API que necesita una sesión de base de datos para la solicitud actual o un registrador con contexto específico de la solicitud.
Con las dependencias del ámbito, el marco crea un contenedor del ámbito de la solicitud cuando la solicitud comienza a resolver todas las dependencias de ese ámbito.
Cuando finaliza la solicitud, se lleva a cabo automáticamente cualquier limpieza necesaria, como cerrar sesiones de la base de datos. Esto garantiza que los estados no se compartan entre solicitudes y evita fugas de recursos.
Inyección asíncrona
Para gestionar tareas vinculadas a E/S de forma eficiente, necesitas una ejecución asíncrona. Los sistemas de inyección de dependencias en marcos como FastAPI y dependency-injector ofrecen inyección asíncrona mediante async/await.
La inyección asíncrona se refiere a la inyección de dependencias que son funciones e async def es o a la gestión de e async resources es como conexiones de bases de datos asíncronas, API externas o tareas en segundo plano.
En lugar de resolverlas de forma sincrónica, el sistema de inyección de dependencias espera a que se resuelvan antes de inyectarlas.
El siguiente es un ejemplo de una dependencia asíncrona en 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}Con la tecnología de
FastAPI gestiona automáticamente la resolución asíncrona de get_user e inyecta el resultado.
dependency-injector También admite la gestión asíncrona del ciclo de vida mediante proveedores de eventos ( 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()Con la tecnología de
En el código anterior:
resources.AsyncResource Se utiliza para la lógica de configuración/desmontaje asíncrono.
await container.init_resources() inicializa todos los recursos y llama a init.
await container.shutdown_resources() limpia todos los recursos.
await container.db() obtiene la instancia real de los recursos, que es AsyncDB.
El uso de la inyección de dependencias asíncronas ayuda a mantener el rendimiento de la aplicación al evitar la E/S sincrónica en rutas asíncronas y elimina la necesidad de esperar manualmente las dependencias en la lógica.
Implicaciones para la seguridad
Si bien la inyección de dependencias mejora la modularidad y la capacidad de prueba, puede introducir riesgos potenciales de seguridad si no se gestiona con cuidado.
Un riesgo importante es la confusión de dependencias, en la que paquetes maliciosos imitan a otros legítimos. Por ejemplo, los atacantes han subido paquetes a PyPI con nombres utilizados en sistemas empresariales internos para engañar a las aplicaciones y que utilicen la versión maliciosa.
Para mantener la seguridad, asegúrate de no inyectar nunca componentes que no sean de confianza o controlados por el usuario y valida las fuentes de configuración, como las variables de entorno, antes de inyectarlas.
Utiliza los archivos « requirements.txt » o « poetry.lock » para bloquear versiones exactas y automatizar el análisis de seguridad cuando se produzcan cambios en las dependencias. Además, asegúrate de auditar periódicamente tus dependencias en busca de vulnerabilidades.
Centralizar el control de tus dependencias también es esencial, ya que garantiza la coherencia entre tus aplicaciones y facilita la aplicación de parches de seguridad o actualizaciones de versiones. Ayuda a evitar las «dependencias ocultas» inyectadas de diferentes maneras en el código base.
Existen varias herramientas que pueden detectar riesgos y mejorar tu postura de seguridad:
Snyk: Busca vulnerabilidades conocidas en paquetes Python y sugiere soluciones.
Xygeni: Ofrece protección para los procesos de CI/CD.
Seguridad: Comprueba si hay paquetes inseguros y vulnerabilidades en el código.
Bandit: Un analizador de código estático para Python centrado en cuestiones de seguridad.
Aplicaciones en el mundo real
La inyección de dependencias no es solo un patrón teórico, sino que se utiliza en diversos sistemas de producción para gestionar la complejidad, mejorar la flexibilidad y optimizar las pruebas.
Configuración del servicio web
En las aplicaciones web modernas, la inyección de dependencias desempeña un papel crucial en la gestión de servicios estándar, como la autenticación, el registro y el acceso a bases de datos.
Los marcos como FastAPI desempeñan un papel esencial en la resolución de rutas y dependencias.
El siguiente ejemplo ilustra una implementación de autenticación centralizada en 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']}"}Con la tecnología de
En el código anterior, get_current_user() se inyecta como una dependencia de ruta, pero la lógica para verificar el token está centralizada y es reutilizable.
También puedes hacer lo mismo para el acceso a la base de datos, de modo que las sesiones de la base de datos se gestionen y limpien automáticamente.
A continuación se muestra un ejemplo de cómo puedes utilizar la inyección de dependencias para centralizar el acceso a la base de datos.
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")Con la tecnología de
En el código anterior, get_db_session es un proveedor de dependencias. Cuando se llama a la ruta read_items, FastAPI inyecta automáticamente el parámetro db, lo que permite a la ruta acceder a la base de datos sin tener que gestionar manualmente la configuración o el cierre de la sesión.
Desarrollo basado en pruebas
La inyección de dependencias facilita el cambio de las dependencias que reciben los componentes, lo cual es importante a la hora de realizar pruebas.
Puedes sustituir servicios reales por simulaciones durante las pruebas,lo que hace que tu código sea más modular y fácil de probar.
FastAPI y dependency-injector admiten la sustitución de dependencias en tiempo de prueba.
Aquí tienes un ejemplo en 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 ==200Con la tecnología de
En el ejemplo de código anterior, get_db_session se sobrescribe con una base de datos simulada durante las pruebas; por lo tanto, no es necesario parchear ni modificar el código de producción.
Lo mismo ocurre en depedency-injector, donde puedes sustituir tu base de datos real por una base de datos ficticia con fines de prueba, como en el ejemplo siguiente.
from containers import Container
deftest_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()assert service.get_data()=="mocked"Con la tecnología de
El método « override() » sustituye la dependencia real por una simulación, que es limpia, controlada y reversible.
A continuación, se indican algunas estrategias que debes utilizar para anular las dependencias:
Inyección del constructor: Asegúrate de pasar las simulaciones directamente al constructor.
Anulaciones del marco: Utiliza herramientas de anulación integradas, como FastAPI.dependency_overrides en FastAPI o dependency_injector.override() en dependency-injector.
Partidos: Utiliza marcos de pruebas como pytest para crear simulaciones reutilizables e insertarlas a través de accesorios.
Mejores prácticas y antipatrones
Aunque la inyección de dependencias tiene como objetivo hacer que el código sea modular y fácil de probar, puede provocar errores o crear complejidad en el código.
Prácticas recomendadas
A continuación, se indican algunas prácticas recomendadas que debes seguir al crear dependencias:
Programa para interfaces, no para implementaciones: Define tus dependencias utilizando clases base abstractas o interfaces en lugar de implementaciones complejas. Esto facilita la comprobación del código y fomenta el uso de simulaciones en las pruebas unitarias.
Preferimos la composición a la herencia: En lugar de crear jerarquías de clases complejas, compón objetos utilizando dependencias inyectadas para mantener los componentes pequeños y centrados.
Configuración centralizada de dependencias: Utiliza un único contenedor o módulo para definir y gestionar las dependencias, lo que facilita la sustitución y las pruebas.
Evita las dependencias circulares: Diseña cuidadosamente tu gráfico de dependencias para evitar referencias circulares utilizando funciones de fábrica o proveedores para retrasar la instanciación.
Errores comunes
A continuación, se indican algunos errores comunes que debes evitar al utilizar la inyección de dependencias.
Antipatrón del localizador de servicios: Evita utilizar un contenedor global para obtener dependencias de forma dinámica, ya que esto puede ocultar dependencias y complicar la comprensión y las pruebas del código.
Sobredosificación: Evita inyectar demasiados servicios en una sola clase o función para evitar que tu código dé lugar a constructores inflados y a una lógica difícil de probar.
Mala gestión del alcance y fugas estatales: Asegúrate de utilizar siempre el ámbito adecuado para evitar compartir el estado entre usuarios o pruebas.
Pruebas de inyección de dependencias en Python
La inyección de dependencias facilita la inserción de objetos simulados en tus componentes, lo que simplifica las pruebas unitarias. Esto te permite separar los componentes fundamentales de los específicos de la prueba.
Con la inyección de dependencias, las pruebas son deterministas, no tienen efectos secundarios y se ejecutan más rápidamente.
A continuación se muestra un ejemplo de cómo puedes utilizar la inyección de dependencias para inyectar versiones simuladas, stubs o falsas de esas dependencias en tus pruebas.
classEmailService:defsend_email(self, to:str, message:str):...classNotifier:def__init__(self, email_service: EmailService):
self.email_service = email_service
defalert(self, user_email):
self.email_service.send_email(user_email,"Alert!")Con la tecnología de
En las pruebas, sustituye EmailService por una simulación:
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]"Con la tecnología de
Asegúrate de utilizar con frecuencia la inyección de dependencias y evita los parches improvisados para facilitar el mantenimiento y evitar cambios importantes cuando se modifiquen los nombres internos.
Conclusión
Aunque es posible que al principio no necesites la inyección de dependencias para proyectos pequeños, a medida que el proyecto crezca y se amplíe, podrás beneficiarte de la estructura y la modularidad que ofrece la inyección de dependencias.
De cara al futuro, la inyección de dependencias en Python es prometedora, y podemos esperar que continúe con un sistema de tipos mejorado y una gestión del ciclo de vida asíncrono optimizada.
Dominar la inyección de dependencias es esencial para convertirse en un programador Python fluido. Pero eso no es todo: hay muchos otros conceptos avanzados que pueden mejorar tus habilidades en Python.
Para ayudarte a acelerar tu proceso de aprendizaje, aquí tienes algunos de nuestros recursos que te recomendamos consultar.
Instructor experimentado en ciencia de datos y bioestadístico con experiencia en Python, R y aprendizaje automático.
Preguntas frecuentes
¿Qué es la inyección de dependencias?⌄
La inyección de dependencias es un patrón de diseño que te permite escribir código más limpio mediante la creación de dependencias dentro de una clase, que pueden pasarse desde el exterior, lo que facilita la gestión y modificación del código.
¿Cuáles son las principales ventajas de la inyección de dependencias?⌄
La inyección de dependencias hace que tu código sea modular, comprobable y fácil de mantener.
¿Cómo puedo implementar la inyección de dependencias en Python?⌄
Puedes implementar la inyección de dependencias en Python de forma manual o utilizando bibliotecas como dependency_injector.
¿Qué es la inyección asíncrona?⌄
La inyección asíncrona se refiere a la inyección de dependencias que son funciones e async def es o a la gestión de e async resources es como conexiones de bases de datos asíncronas, API externas o tareas en segundo plano.
¿Cuáles son los riesgos de seguridad de la inyección de dependencias y cómo puedo gestionarlos?⌄
Un riesgo importante es la confusión de dependencias, en la que paquetes maliciosos imitan a otros legítimos. Para evitarlo, puedes utilizar herramientas como Snyk, Xygeni, Safety o Bandit para analizar tu código y tus paquetes en busca de vulnerabilidades.
Aprende a crear e importar módulos de Python. ¡Descubre las mejores prácticas, ejemplos y consejos para escribir código Python reutilizable, organizado y eficiente!
Esta guía completa te enseña los fundamentos del backend con Python. Aprende conceptos básicos, frameworks y buenas prácticas para empezar a crear aplicaciones web.
Aborda los fundamentos de la Programación Orientada a Objetos (POO) en Python: explora las clases, los objetos, los métodos de instancia, los atributos y ¡mucho más!
La inyección de dependencias es un patrón de diseño en el que los objetos o servicios (nos referimos a las dependencias) se proporcionan a una clase desde el exterior, en lugar de que la clase los cree internamente. Esto desacopla los componentes.
El uso de la inyección de dependencias en Python tiene muchas ventajas, tales como:
En este tutorial, te mostraré cómo utilizar la inyección de dependencias en Python a través de ejemplos sencillos y prácticos.
Comprender la inyección de dependencias en Python
Para comprender cómo implementar la inyección de dependencias, es esencial que comprendas los principios clave que la rigen.
La inyección de dependencias se basa en algo llamado principio de inversión de control (IoC), que significa que, en lugar de que una clase cree y gestione sus dependencias, las recibe de una fuente externa. Esto ayuda a centrarse en una sola tarea y hace que tu código sea más limpio.
La inyección de dependencias en Python incluye estas formas comunes, aunque hay otras:
¿Por qué utilizar la inyección de dependencias en Python?
Como programador, comprender e implementar la inyección de dependencias puede marcar una gran diferencia en el diseño de tu código. Veamos algunas ventajas de la inyección de dependencias.
Modularidad, reutilización y flexibilidad
La inyección de dependencias te permite dividir el código en componentes más pequeños y específicos, lo que proporciona una mayor flexibilidad y modularidad.
Cada parte o componente se encarga de una tarea específica y depende de elementos externos, lo que facilita su reutilización en diferentes partes de tu aplicación o de la de otra persona.
Por ejemplo, si tienes una clase que envía mensajes. En lugar de integrar una lógica de correo electrónico o SMS directamente en tu servicio, puedes insertar una clase de remitente de correo electrónico o SMS.
Ahora, tu clase que envía mensajes no se preocupa por cómo se envía el mensaje, sino que simplemente utiliza el remitente que se le proporciona. Esto implica que puedes utilizar tu remitente de mensajes con diferentes remitentes de correo electrónico o SMS.
Mantenibilidad y capacidad de prueba
Al utilizar la inyección de dependencias, puedes sustituir fácilmente las dependencias reales por datos simulados durante las pruebas. Esto simplifica la escritura de pruebas unitarias sin depender de API o bases de datos.
Supongamos, como ejercicio de reflexión, que tienes una clase que guarda datos en una base de datos. A efectos de prueba, puedes inyectar una base de datos ficticia en lugar de una real para evitar que la prueba quede vinculada a la base de datos de producción.
La inyección de dependencias te permite modificar partes específicas de tu código sin romper todo el sistema. Esto resulta especialmente útil en situaciones en las que necesitas cambiar de proveedor de pagos en una aplicación, por ejemplo, de Stripe a PayPal, sin modificar el resto del código.
Inversión de control y dependencias intercambiables
La idea de la inversión de control es la siguiente: la inyección de dependencias trasladará la responsabilidad de crear y gestionar las dependencias a una fuente externa.
Esto implica que puedes cambiar fácilmente una implementación por otra; por ejemplo, si tienes una clase
ReportGeneratorque formatea y exporta informes.En lugar de que la clase decida el formato de exportación, podrías insertar una clase exportadora, como
PDFExporter,ExcelExporteroCSVExporter.A la clase `
ReportGenerator` no le importa cómo se realiza la exportación, por lo que para cambiar un formato, solo tienes que cambiar el exportador que inyectas, sin modificar en absoluto el `ReportGenerator`.Implementación de la inyección de dependencias en Python
Ahora que tienes una idea clara de qué es la inyección de dependencias y por qué es útil, veamos cómo puedes implementarla en Python. Comencemos con el método más sencillo: la inyección manual de dependencias.
Inyección manual de dependencias
En la inyección manual de dependencias, tú mismo creas las dependencias y las pasas a la clase o función que las necesita.
Este método funciona bien para aplicaciones pequeñas, pero a medida que tu aplicación crece, es necesario utilizar un marco de inyección de dependencias para evitar errores.
Aquí tienes un ejemplo de código sin inyección de dependencias.
En el ejemplo anterior,
UserNotifierestá estrechamente vinculado aEmailService. No es fácil intercambiar o simularEmailServicepara realizar pruebas.Aquí hay otra versión, pero con inyección de dependencias manual.
La versión anterior es flexible y te permite inyectar varios servicios de mensajería; también puedes sustituir
EmailServicepor una simulación en las pruebas.Uso del marco de inyección de dependencias
A medida que tu aplicación crece, gestionar las dependencias se vuelve complejo y complicado.
El
dependency-injectormarco proporciona un enfoque estructurado.Arquitectura de proveedor de contenedores
El marco de trabajo de control de enrutamiento de eventos (
dependency-injector) funciona basándose en una arquitectura de contenedor-proveedor.En el ejemplo anterior:
Containerdefine cómo se crean las instanciasSingletongarantiza que solo exista una instancia deEmailServiceFactorycrea un nuevo objeto de control de navegación (UserNotifier) con el control de navegación de la página principal (EmailService) inyectado automáticamente.Mecanismo de cableado
Puedes utilizar decoradores como
@injectpara reducir la necesidad de pasar dependencias manualmente.Aquí hay otra versión del ejemplo anterior:
En la versión actual;
Sin añadir manualmente la dependencia, el decorador «
@inject» inyecta automáticamente la dependencia de la función o clase.La clase «
Provide» indica a «dependency-injector» qué dependencia debe inyectar.Este enfoque elimina la necesidad de dependencias de cableado manual desde el contenedor.
Es útil en aplicaciones más grandes, donde los servicios pueden depender de varios componentes (de ahí la necesidad de inicializarlos automáticamente).
Uso de la inyección de dependencias en marcos populares de Python
La mayoría de los frameworks modernos de Python facilitan el trabajo con la inyección de dependencias, lo que te permite escribir código más limpio y fácil de probar.
En esta sección, aprenderás cómo se aplica la inyección de dependencias en Flask, Django y FastAPI.
Inyección de dependencias en Flask
Flask no tiene soporte integrado para la inyección de dependencias, pero puedes implementarla utilizando la biblioteca
dependency-injectorsiguiendo estos pasos.Paso 1: Define tus servicios
Paso 2: Configura tu contenedor de inyección de dependencias
Paso 3: Crea la aplicación Flask e inyecta las dependencias.
Del código anterior:
La función
name = request.args.get(“name”, “World”)extraenamede la cadena de consulta y, si no se proporciona, utiliza”World”como valor predeterminado.return greeting_service.get_greeting(name)llama al método en el servicio inyectado y devuelve un mensaje de saludo.container.wire(modules=[__name__])indica al inyector de dependencias que busque en este módulo decoradores de inyección de dependencias (@inject) y marcadores de inyección de dependencias (Provide[...]) para conectarlos a los proveedores reales.Paso 4: Prueba la aplicación
Ejecuta la aplicación en tu terminal utilizando el comando
python .pyy visita la URLhttp://localhost:5000/greet?name=Jacob.Deberías ver lo siguiente.
Inyección de dependencias en Django
Al igual que Flask, Django no ofrece inyección de dependencias integrada. Sin embargo, puedes utilizar sus vistas basadas en clases, middleware y estructura de aplicaciones para integrar la inyección de dependencias manualmente o utilizando la biblioteca
dependency-injector.A continuación se muestra un procedimiento paso a paso para implementar la inyección de dependencias en Django.
Paso 1: Define tu servicio
Crea un servicio sencillo que contenga la lógica de tu negocio en un nuevo archivo
services.pydentro de la carpeta de tu aplicación.Paso 2: Configurar el contenedor del inyector de dependencias
Dentro del directorio de la aplicación, crea un contenedor utilizando la biblioteca
dependency-injectordentro de un nuevo archivocontainers.py.Paso 3: Crear una vista con dependencias inyectadas
Utiliza el decorador «
@inject» en tu vista para recibir las dependencias en «views.py».Paso 4: Actualiza urls.py
En la carpeta del proyecto, ve al archivo
urls.pyy conecta el inyector de dependencias al sistema de enrutamiento de URL de Django.Paso 5: Conecta el contenedor en apps.py
Ve a
apps.pyy conecta el contenedor a la aplicación.A continuación, ve a la configuración de la aplicación en
INSTALLED_APPSy añade tu aplicación.Paso 6: Probar la vista
Inicia tu servidor Django y accede a la vista a través de la URL
http://localhost:8000/greet/?name=Django.Deberías ver lo siguiente.
Inyección de dependencias en FastAPI
FastAPI tiene soporte integrado para la inyección de dependencias a través del marcador especial
Depends(), que inyecta cualquier cosa que devuelva una función.A continuación, se incluye una guía paso a paso sobre cómo realizar la inyección de dependencias en FastAPI.
Paso 1: Define un servicio
Paso 2: Crear la función de dependencia
Paso 3: Configurar la aplicación FastAPI y utilizar la inyección de dependencias
Paso 4: Ejecuta y prueba la aplicación.
Inicia tu servidor con el comando
uvicorn main:app –reloady visita la URLhttp://localhost:8000/greet?name=FastAPI. Deberías ver la siguiente respuesta.Comparación de marcos de inyección de dependencias en Python
Existen varios marcos de dependencias de Python, cada uno con sus propias ventajas, sintaxis y casos de uso. La elección del marco adecuado depende del tamaño y la estructura de tu proyecto.
A continuación se muestra una comparación de algunos marcos comunes de inyección de dependencias en Python.
Marco
Mejor Para
Puntos fuertes
Limitaciones
dependency-injectorAplicaciones complejas en uso productivo
Completo, rápido y configurable
Verboso
injectorSencillo y adecuado para aplicaciones mínimas.
Sintaxis limpia
Menos avanzado que otros.
pinjectLógica de enlace avanzada
Cableado automático
Más lento debido a la introspección
punqAplicaciones sencillas
Mínimo y rápido
Funciones limitadas en comparación con otros
FastAPI
Depends()Aplicaciones FastAPI
Compatibilidad integrada con async y también se puede probar.
No reutilizable fuera de FastAPI.
Flask-injectorAplicaciones Flask
Fácil integración con aplicaciones Flask
Depende de
injectordjango-injectorProyectos Django
Proporciona inyección de dependencias para vistas y middleware.
Depende de
injectorPatrones de implementación avanzados
A medida que el código base de tu aplicación se vuelve más grande, la inyección de dependencias se vuelve difícil de mantener.
Puedes utilizar la inyección de dependencias avanzada para gestionar tareas como los recursos del ámbito de la solicitud, la limpieza y la separación de preocupaciones.
Dependencias con ámbito
Las dependencias con ámbito te permiten controlar cuánto tiempo persiste una dependencia, por ejemplo, cuando deseas un nuevo objeto por solicitud, un objeto compartido durante toda la vida útil de la aplicación o la limpieza automática de recursos como las conexiones a bases de datos.
La mayoría de los marcos de inyección de dependencias admiten ámbitos como:
A continuación se muestra un ejemplo de dependencias con ámbito y contenedores anidados utilizando
dependency-injector.En el ejemplo anterior,
MyResourceimita una clase que requiere configuración y desmontaje, como abrir/cerrar un archivo o una base de datos.El controlador de recursos de clase única (
container) proporciona el recurso medianteproviders.Resourcey, cuando utilizaswith container.resource(), entra y sale del contexto correctamente.Este patrón garantiza que los recursos, como los manejadores de archivos, las conexiones a bases de datos o las sesiones de red, se gestionen de forma limpia.
Imagina un punto final de API que necesita una sesión de base de datos para la solicitud actual o un registrador con contexto específico de la solicitud.
Con las dependencias del ámbito, el marco crea un contenedor del ámbito de la solicitud cuando la solicitud comienza a resolver todas las dependencias de ese ámbito.
Cuando finaliza la solicitud, se lleva a cabo automáticamente cualquier limpieza necesaria, como cerrar sesiones de la base de datos. Esto garantiza que los estados no se compartan entre solicitudes y evita fugas de recursos.
Inyección asíncrona
Para gestionar tareas vinculadas a E/S de forma eficiente, necesitas una ejecución asíncrona. Los sistemas de inyección de dependencias en marcos como FastAPI y
dependency-injectorofrecen inyección asíncrona medianteasync/await.La inyección asíncrona se refiere a la inyección de dependencias que son funciones e
async defes o a la gestión de easync resourceses como conexiones de bases de datos asíncronas, API externas o tareas en segundo plano.En lugar de resolverlas de forma sincrónica, el sistema de inyección de dependencias espera a que se resuelvan antes de inyectarlas.
El siguiente es un ejemplo de una dependencia asíncrona en FastAPI:
FastAPI gestiona automáticamente la resolución asíncrona de
get_usere inyecta el resultado.dependency-injectorTambién admite la gestión asíncrona del ciclo de vida mediante proveedores de eventos (AsyncResource).En el código anterior:
resources.AsyncResourceSe utiliza para la lógica de configuración/desmontaje asíncrono.await container.init_resources()inicializa todos los recursos y llama ainit.await container.shutdown_resources()limpia todos los recursos.await container.db()obtiene la instancia real de los recursos, que esAsyncDB.El uso de la inyección de dependencias asíncronas ayuda a mantener el rendimiento de la aplicación al evitar la E/S sincrónica en rutas asíncronas y elimina la necesidad de esperar manualmente las dependencias en la lógica.
Implicaciones para la seguridad
Si bien la inyección de dependencias mejora la modularidad y la capacidad de prueba, puede introducir riesgos potenciales de seguridad si no se gestiona con cuidado.
Un riesgo importante es la confusión de dependencias, en la que paquetes maliciosos imitan a otros legítimos. Por ejemplo, los atacantes han subido paquetes a PyPI con nombres utilizados en sistemas empresariales internos para engañar a las aplicaciones y que utilicen la versión maliciosa.
Para mantener la seguridad, asegúrate de no inyectar nunca componentes que no sean de confianza o controlados por el usuario y valida las fuentes de configuración, como las variables de entorno, antes de inyectarlas.
Utiliza los archivos «
requirements.txt» o «poetry.lock» para bloquear versiones exactas y automatizar el análisis de seguridad cuando se produzcan cambios en las dependencias. Además, asegúrate de auditar periódicamente tus dependencias en busca de vulnerabilidades.Centralizar el control de tus dependencias también es esencial, ya que garantiza la coherencia entre tus aplicaciones y facilita la aplicación de parches de seguridad o actualizaciones de versiones. Ayuda a evitar las «dependencias ocultas» inyectadas de diferentes maneras en el código base.
Existen varias herramientas que pueden detectar riesgos y mejorar tu postura de seguridad:
Aplicaciones en el mundo real
La inyección de dependencias no es solo un patrón teórico, sino que se utiliza en diversos sistemas de producción para gestionar la complejidad, mejorar la flexibilidad y optimizar las pruebas.
Configuración del servicio web
En las aplicaciones web modernas, la inyección de dependencias desempeña un papel crucial en la gestión de servicios estándar, como la autenticación, el registro y el acceso a bases de datos.
Los marcos como FastAPI desempeñan un papel esencial en la resolución de rutas y dependencias.
El siguiente ejemplo ilustra una implementación de autenticación centralizada en FastAPI.
En el código anterior,
get_current_user()se inyecta como una dependencia de ruta, pero la lógica para verificar el token está centralizada y es reutilizable.También puedes hacer lo mismo para el acceso a la base de datos, de modo que las sesiones de la base de datos se gestionen y limpien automáticamente.
A continuación se muestra un ejemplo de cómo puedes utilizar la inyección de dependencias para centralizar el acceso a la base de datos.
En el código anterior,
get_db_sessiones un proveedor de dependencias. Cuando se llama a la rutaread_items, FastAPI inyecta automáticamente el parámetrodb, lo que permite a la ruta acceder a la base de datos sin tener que gestionar manualmente la configuración o el cierre de la sesión.Desarrollo basado en pruebas
La inyección de dependencias facilita el cambio de las dependencias que reciben los componentes, lo cual es importante a la hora de realizar pruebas.
Puedes sustituir servicios reales por simulaciones durante las pruebas,lo que hace que tu código sea más modular y fácil de probar.
FastAPI y
dependency-injectoradmiten la sustitución de dependencias en tiempo de prueba.Aquí tienes un ejemplo en FastAPI.
En el ejemplo de código anterior,
get_db_sessionse sobrescribe con una base de datos simulada durante las pruebas; por lo tanto, no es necesario parchear ni modificar el código de producción.Lo mismo ocurre en
depedency-injector, donde puedes sustituir tu base de datos real por una base de datos ficticia con fines de prueba, como en el ejemplo siguiente.El método «
override()» sustituye la dependencia real por una simulación, que es limpia, controlada y reversible.A continuación, se indican algunas estrategias que debes utilizar para anular las dependencias:
FastAPI.dependency_overridesen FastAPI odependency_injector.override()endependency-injector.pytestpara crear simulaciones reutilizables e insertarlas a través de accesorios.Mejores prácticas y antipatrones
Aunque la inyección de dependencias tiene como objetivo hacer que el código sea modular y fácil de probar, puede provocar errores o crear complejidad en el código.
Prácticas recomendadas
A continuación, se indican algunas prácticas recomendadas que debes seguir al crear dependencias:
Errores comunes
A continuación, se indican algunos errores comunes que debes evitar al utilizar la inyección de dependencias.
Pruebas de inyección de dependencias en Python
La inyección de dependencias facilita la inserción de objetos simulados en tus componentes, lo que simplifica las pruebas unitarias. Esto te permite separar los componentes fundamentales de los específicos de la prueba.
Con la inyección de dependencias, las pruebas son deterministas, no tienen efectos secundarios y se ejecutan más rápidamente.
A continuación se muestra un ejemplo de cómo puedes utilizar la inyección de dependencias para inyectar versiones simuladas, stubs o falsas de esas dependencias en tus pruebas.
En las pruebas, sustituye
EmailServicepor una simulación:Asegúrate de utilizar con frecuencia la inyección de dependencias y evita los parches improvisados para facilitar el mantenimiento y evitar cambios importantes cuando se modifiquen los nombres internos.
Conclusión
Aunque es posible que al principio no necesites la inyección de dependencias para proyectos pequeños, a medida que el proyecto crezca y se amplíe, podrás beneficiarte de la estructura y la modularidad que ofrece la inyección de dependencias.
De cara al futuro, la inyección de dependencias en Python es prometedora, y podemos esperar que continúe con un sistema de tipos mejorado y una gestión del ciclo de vida asíncrono optimizada.
Dominar la inyección de dependencias es esencial para convertirse en un programador Python fluido. Pero eso no es todo: hay muchos otros conceptos avanzados que pueden mejorar tus habilidades en Python.
Para ayudarte a acelerar tu proceso de aprendizaje, aquí tienes algunos de nuestros recursos que te recomendamos consultar.
Cursos:
Entradas del blog: