
He estado escribiendo en Python durante cinco años, de los cuales los últimos tres años han estado desarrollando mi propio proyecto. La mayor parte de esta manera, mi equipo me ayuda con esto. Y con cada versión, con cada nueva característica, estamos tratando cada vez más de asegurarnos de que el proyecto no se convierta en un desastre debido al código no compatible; luchamos con importaciones cíclicas, dependencias mutuas, asignamos módulos reutilizados, reconstruimos la estructura.
Desafortunadamente, en la comunidad de Python no existe un concepto universal de "buena arquitectura", solo existe el concepto de "pitonicidad", por lo que tenemos que idear la arquitectura nosotros mismos. Bajo el corte, Longrid con reflexiones sobre la arquitectura y, en primer lugar, sobre la gestión de dependencias, es aplicable a Python.
django.setup ()
Comenzaré con una pregunta a los dzhangistas. ¿Escribes a menudo estas dos líneas?
import django django.setup()
Debe iniciar el archivo a partir de esto si desea trabajar con objetos django sin iniciar el servidor web django. Esto se aplica a los modelos y herramientas para trabajar con el tiempo (
django.utils.timezone
) y las
django.urls.reverse
(
django.urls.reverse
), y mucho más. Si esto no se hace, obtendrá un error:
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
Constantemente escribo estas dos líneas. Soy un gran admirador del código de expulsión; Me gusta crear un archivo
.py
separado, torcer cosas en él, descifrarlo y luego incrustarlo en el proyecto.
Y esta constante
django.setup()
me molesta mucho. En primer lugar, te cansas de repetirlo en todas partes; y, en segundo lugar, la inicialización de django lleva unos segundos (tenemos un gran monolito), y cuando reinicia el mismo archivo 10, 20, 100 veces, simplemente ralentiza el desarrollo.
¿Cómo deshacerse de
django.setup()
? Necesita escribir código que dependa mínimamente de django.
Por ejemplo, si escribimos un cliente de una API externa, entonces podemos hacerlo dependiente de django:
from django.conf import settings class APIClient: def __init__(self): self.api_key = settings.SOME_API_KEY
o puede ser independiente de django:
class APIClient: def __init__(self, api_key): self.api_key = api_key
En el segundo caso, el constructor es más engorroso, pero cualquier manipulación con esta clase se puede hacer sin cargar toda la maquinaria dzhangovskoy.
Las pruebas también se están volviendo más fáciles. ¿Cómo probar un componente que depende de la configuración de
django.conf.settings
? Solo ciérrelos con el decorador
@override_settings
. Y si el componente no depende de nada, entonces no habrá nada que mojar: pasó los parámetros al constructor y lo condujo.
Gestión de dependencias
La historia de dependencia de
django
es el ejemplo más sorprendente de un problema que encuentro todos los días: problemas de administración de dependencias en python, y la arquitectura general de las aplicaciones de python.
La relación con la gestión de dependencias en la comunidad Python es mixta. Se pueden distinguir tres campamentos principales:
- Python es un lenguaje flexible. Escribimos como queramos, dependiendo de lo que queramos. No somos tímidos con respecto a las dependencias cíclicas, la sustitución de atributos para clases en tiempo de ejecución, etc.
- Python es un lenguaje especial. Hay formas idiomáticas de construir arquitectura y dependencias. La transferencia de datos hacia arriba y hacia abajo en la pila de llamadas es realizada por iteradores, corutinas y administradores de contexto.
Informe de clase sobre este tema y ejemploBrandon Rhodes, Dropbox:
Eleva tu IO .
Ejemplo del informe:
def main(): """ """ with open("/etc/hosts") as file: for line in parse_hosts(file): print(line) def parse_hosts(lines): """ - """ for line in lines: if line.startswith("#"): continue yield line
- La flexibilidad de Python es una forma adicional de dispararte en el pie. Necesita un conjunto rígido de reglas para administrar dependencias. Un buen ejemplo son los chicos rusos de pitón seco . Todavía hay un enfoque menos duro: la estructura de Django para escala y longevidad , pero la idea es la misma.
Hay varios artículos sobre gestión de dependencias en python (
ejemplo 1 ,
ejemplo 2 ), pero todos se reducen a anunciar los marcos de inyección de dependencias de alguien. Este artículo es una nueva entrada sobre el mismo tema, pero esta vez es un experimento de pensamiento puro sin publicidad. Este es un intento de encontrar un equilibrio entre los tres enfoques anteriores, prescindir de un marco adicional y hacerlo "pitónico".
Recientemente leí
Clean Architecture , y parece que entiendo cuál es el valor de la inyección de dependencia en python y cómo se puede implementar. Vi esto en el ejemplo de mi propio proyecto. En pocas palabras, esto está
protegiendo el código para que no se rompa cuando otro código cambia .
Datos de origen
Hay un cliente API que ejecuta solicitudes HTTP para el acortador de servicios:
Y hay un módulo que acorta todos los enlaces en el texto. Para hacer esto, usa el cliente API acortador:
La lógica de la ejecución del código vive en un archivo de control separado (llamémoslo controlador):
Todo funciona El procesador analiza el texto, acorta los enlaces con un acortador y devuelve el resultado. Las dependencias se ven así:

El problema
Este es el problema: la clase
TextProcessor
depende de la clase
TextProcessor
, y se
interrumpe cuando cambia la interfaz de ShortenerClient
.
¿Cómo puede pasar esto?
Supongamos que en nuestro proyecto decidimos rastrear
shorten_link
y agregamos el argumento
callback_url
al método
shorten_link
. Este argumento significa la dirección a la que deben llegar las notificaciones al hacer clic en un enlace.
El método
ShortenerClient.shorten_link
comenzó a verse así:
def shorten_link(self, url, callback_url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url, 'callback_on_click': callback_url} ) return response.json()['url']
Y que pasa Y resulta que cuando intentamos comenzar, recibimos un error:
TypeError: shorten_link() missing 1 required positional argument: 'callback_url'
Es decir, cambiamos el acortador, pero no fue él quien rompió, sino su cliente:

¿Y qué? Bueno, el archivo de llamadas se rompió, fuimos y lo arreglamos. Cual es el problema
Si esto se resuelve en un minuto, fueron y se corrigieron, entonces esto, por supuesto, no es un problema en absoluto. Si hay poco código en las clases y si los admite usted mismo (este es su proyecto paralelo, estas son dos clases pequeñas del mismo subsistema, etc.), puede detenerse allí.
Los problemas comienzan cuando:
- los módulos que llaman y los que llaman tienen mucho código;
- diferentes módulos son compatibles con diferentes personas / equipos.
Si escribe la clase
TextProcessor
y su colega escribe
TextProcessor
, obtendrá una situación ofensiva:
cambió el código, pero se rompió. Y se rompió en un lugar que no has visto en la vida, y ahora necesitas sentarte y entender el código de otra persona.
Aún más interesante es cuando su módulo se usa en varios lugares, y no en uno; y su edición romperá el código en el montón de archivos.
Por lo tanto, el problema se puede formular de la siguiente manera: ¿cómo organizar el código para que cuando se cambie la interfaz de
ShortenerClient
,
ShortenerClient
ShortenerClient
y no sus consumidores (de los cuales puede haber muchos)?
La solución aquí es:
- Los consumidores de la clase y la clase misma deben acordar una interfaz común. Esta interfaz debería convertirse en ley.
- Si la clase deja de corresponder a su interfaz, serán sus problemas y no los problemas de los consumidores.

Congelar la interfaz
¿Cómo se ve arreglar una interfaz en Python? Esta es una clase abstracta:
from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key): pass @abstractmethod def shorten_link(self, link): pass
Si ahora heredamos de esta clase y olvidamos implementar algún método, obtendremos un error:
class ShortenerClient(AbstractClient): def __ini__(self, api_key): self.api_key = api_key client = ShortenerClient('123') >>> TypeError: Can't instantiate abstract class ShortenerClient with abstract methods __init__, shorten_link
Pero esto no es suficiente. Una clase abstracta captura solo los nombres de los métodos, pero no su firma.
Necesita una segunda herramienta de verificación de firma Esta segunda herramienta es
mypy
. Ayudará a verificar las firmas de los métodos heredados. Para hacer esto, debemos agregar anotaciones a la interfaz:
Si ahora verificamos este código con
mypy
, obtenemos un error debido al argumento adicional
callback_url
:
mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient"
Ahora tenemos una forma confiable de confirmar la interfaz de la clase.
Inversión de dependencia
Después de depurar la interfaz, debemos moverla a otro lugar para eliminar por completo la dependencia del consumidor del archivo
shortener_client.py
. Por ejemplo, puede arrastrar la interfaz directamente al consumidor, a un archivo con el procesador
TextProcessor
:
¡Y eso cambiará la dirección de la adicción! Ahora el
TextProcessor
posee la interfaz de interacción y, como resultado,
ShortenerClient
depende de ella, y no al revés.

En palabras simples, podemos describir la esencia de nuestra transformación de la siguiente manera:
TextProcessor
dice: Soy un procesador y estoy involucrado en la conversión de texto. No quiero saber nada sobre el mecanismo de acortamiento: este no es asunto mío. Quiero extraer el método shorten_link
para que shorten_link
todo para mí. Entonces, por favor, dame un objeto que juegue de acuerdo con mis reglas. Las decisiones sobre cómo interactúo las tomo yo, no él.ShortenerClient
dice: parece que no puedo existir en el vacío, y ellos requieren cierto comportamiento de mi parte. Voy a preguntarle a TextProcessor
qué necesito para que no coincida.
Múltiples consumidores
Si varios módulos usan enlaces de acortamiento, entonces la interfaz no debe colocarse en uno de ellos, sino en un archivo separado, que se encuentra por encima de los otros archivos, en una jerarquía más alta:

Componente de control
Si los consumidores no importan
ShortenerClient
, ¿quién lo importará y creará un objeto de clase? Debería ser un componente de control, en nuestro caso es
controller.py
.
El enfoque más simple es una inyección de dependencia directa, la inyección de dependencia "en la frente". Creamos objetos en el código de llamada, transferimos un objeto a otro. Ganancia
Enfoque de Python
Se cree que un enfoque más "pitónico" es la inyección de dependencia a través de la herencia.
Raymond Hettinger habla sobre esto con gran detalle en su informe Super considerado Super. Para adaptar el código a este estilo, debe cambiar ligeramente el
TextProcessor
que sea heredable:
Y luego, en el código de llamada, herede:
El segundo ejemplo es omnipresente en los marcos populares:
- En Django, somos constantemente heredados. Redefinimos los métodos de vista basada en clase, modelos, formularios; en otras palabras, inyecta nuestras dependencias en el trabajo ya depurado del framework.
- En DRF, lo mismo. Estamos ampliando vistas, serializadores, permisos.
- Y así sucesivamente. Muchos ejemplos
El segundo ejemplo parece más bonito y más familiar, ¿no? Vamos a desarrollarlo y ver si se conserva esta belleza.
Desarrollo de Python
En la lógica de negocios, generalmente hay más de dos componentes. Supongamos que nuestro
TextProcessor
no es una clase independiente, sino solo uno de los elementos de la
TextPipeline
que procesa el texto y lo envía al correo:
class TextPipeline: def __init__(self, text, email): self.text_processor = TextProcessor(text) self.mailer = Mailer(email) def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text)
Si queremos aislar
TextPipeline
de las clases utilizadas, debemos seguir el mismo procedimiento que antes:
- la clase
TextPipeline
declarará interfaces para los componentes utilizados; - los componentes usados se verán obligados a cumplir con estas interfaces;
- algún código externo juntará todo y se ejecutará.
El diagrama de dependencia se verá así:

Pero, ¿cómo será ahora el código de ensamblaje de estas dependencias?
import TextProcessor import ShortenerClient import Mailer import TextPipeline class ProcessorWithClient(TextProcessor): def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='123') class PipelineWithDependencies(TextPipeline): def get_text_processor(self, text: str) -> ProcessorWithClient: return ProcessorWithClient(text) def get_mailer(self, email: str) -> Mailer: return Mailer(email) pipeline = PipelineWithDependencies( email='abc@def.com', text=' 1: https://ya.ru 2: https://google.com' ) pipeline.process_and_mail()
¿Te has dado cuenta? Primero heredamos la clase
TextProcessor
para insertar el
ShortenerClient
en él, y luego heredamos
TextPipeline
para insertar nuestro
TextProcessor
anulado (así como
Mailer
) en él. Tenemos varios niveles de redefinición secuencial. Ya complicado
¿Por qué todos los marcos están organizados de esta manera?
Sí, porque solo es adecuado para marcos.- Todos los niveles del marco están claramente definidos y su número es limitado. Por ejemplo, en Django, puede anular
FormField
para insertarlo en una anulación de un Form
, para insertar un formulario en una anulación de View
. Eso es todo. Tres niveles - Cada marco tiene un propósito. Esta tarea está claramente definida.
- Cada marco tiene documentación detallada que describe cómo y qué heredar; qué y con qué combinar.
¿Puede identificar y documentar de manera clara e inequívoca su lógica empresarial? ¿Especialmente la arquitectura de los niveles en los que trabaja? Yo no Desafortunadamente, el enfoque de Raymond Hettinger no se adapta a la lógica empresarial.
Volver al enfoque de la frente
En varios niveles de dificultad, gana un enfoque simple. Parece más simple y más fácil de cambiar cuando cambia la lógica.
import TextProcessor import ShortenerClient import Mailer import TextPipeline pipeline = TextPipeline( text_processor=TextProcessor( text=' 1: https://ya.ru 2: https://google.com', shortener_client=ShortenerClient(api_key='abc') ), mailer=Mailer('abc@def.com') ) pipeline.process_and_mail()
Pero, cuando aumenta el número de niveles de lógica, incluso este enfoque se vuelve inconveniente. Tenemos que iniciar imperativamente un grupo de clases, pasándolas entre sí. Quiero evitar muchos niveles de anidamiento.
Probemos una llamada más.
Almacenamiento de instancia global
Intentemos crear un diccionario global en el que se encuentren las instancias de los componentes que necesitamos. Y deje que estos componentes se obtengan entre sí mediante el acceso a este diccionario.
Llamémoslo
INSTANCE_DICT
:
El truco es
poner nuestros objetos en este diccionario antes de acceder a ellos . Esto es lo que haremos en
controller.py
:
Ventajas de trabajar a través de un diccionario global:
- sin magia del capó del motor y marcos DI extra;
- una lista plana de dependencias en las que no necesita administrar el anidamiento;
- todos los bonos DI: pruebas simples, independencia, protección de componentes contra averías cuando otros componentes cambian.
Por supuesto, en lugar de crear
INSTANCE_DICT
, puede usar algún tipo de marco DI; pero la esencia de esto no cambiará. El marco proporcionará una gestión más flexible de las instancias; él te permitirá crearlos en forma de singletones o paquetes, como una fábrica; pero la idea seguirá siendo la misma.
Quizás en algún momento esto no sea suficiente para mí, y todavía elijo algún tipo de marco.
Y, tal vez, todo esto es innecesario, y es más fácil prescindir de él: escribir importaciones directas y no crear interfaces abstractas innecesarias.
¿Cuál es su experiencia con la gestión de dependencias en python? Y en general, ¿es necesario o estoy inventando un problema desde el aire?