
Ich schreibe seit fünf Jahren in Python, von denen die letzten drei Jahre mein eigenes Projekt entwickelt haben. Meistens hilft mir mein Team dabei. Und mit jeder Version und mit jeder neuen Funktion versuchen wir zunehmend sicherzustellen, dass das Projekt nicht durch nicht unterstützten Code zu einem Chaos wird. Wir kämpfen mit zyklischen Importen, gegenseitigen Abhängigkeiten, weisen wiederverwendbare Module zu und bauen die Struktur neu auf.
Leider gibt es in der Python-Community kein universelles Konzept für „gute Architektur“, sondern nur das Konzept für „Pythonalität“. Daher müssen wir uns die Architektur selbst einfallen lassen. Unter dem Strich ist Longrid mit Überlegungen zur Architektur und vor allem zum Abhängigkeitsmanagement auf Python anwendbar.
django.setup ()
Ich beginne mit einer Frage an die Dzhangisten. Schreiben Sie oft diese beiden Zeilen?
import django django.setup()
Sie müssen die Datei von hier aus starten, wenn Sie mit Django-Objekten arbeiten möchten, ohne den Django-Webserver selbst zu starten. Dies gilt für Modelle und Tools zum Arbeiten mit Zeit (
django.utils.timezone
),
django.urls.reverse
(
django.urls.reverse
) und vieles mehr. Wenn dies nicht getan wird, erhalten Sie eine Fehlermeldung:
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
Ich schreibe ständig diese beiden Zeilen. Ich bin ein großer Fan von Auswurfcode. Ich mag es, eine separate
.py
Datei zu erstellen, Dinge darin zu drehen, herauszufinden - und sie dann in das Projekt einzubetten.
Und diese Konstante
django.setup()
nervt mich sehr. Erstens werden Sie es leid, es überall zu wiederholen; und zweitens dauert die Django-Initialisierung einige Sekunden (wir haben einen großen Monolithen), und wenn Sie dieselbe Datei 10, 20, 100 Mal neu starten, verlangsamt dies nur die Entwicklung.
Wie werde ich
django.setup()
los? Sie müssen Code schreiben, der minimal von Django abhängt.
Wenn wir beispielsweise einen Client einer externen API schreiben, können wir ihn von django abhängig machen:
from django.conf import settings class APIClient: def __init__(self): self.api_key = settings.SOME_API_KEY
oder es kann unabhängig von Django sein:
class APIClient: def __init__(self, api_key): self.api_key = api_key
Im zweiten Fall ist der Konstruktor umständlicher, aber alle Manipulationen mit dieser Klasse können durchgeführt werden, ohne die gesamte dzhangovskoy-Maschinerie zu laden.
Tests werden auch einfacher. Wie
django.conf.settings
eine Komponente, die von
django.conf.settings
Einstellungen von
django.conf.settings
abhängt? Sperren Sie sie einfach mit dem Dekorateur
@override_settings
. Und wenn die Komponente von nichts abhängt, gibt es nichts, was nass werden könnte: Sie hat die Parameter an den Konstruktor übergeben - und sie gesteuert.
Abhängigkeitsmanagement
Die
django
Abhängigkeitsgeschichte ist das auffälligste Beispiel für ein Problem, auf das ich jeden Tag
django
: Probleme mit dem Abhängigkeitsmanagement in Python - und die Gesamtarchitektur von Python-Anwendungen.
Die Beziehung zum Abhängigkeitsmanagement in der Python-Community ist gemischt. Drei Hauptlager können unterschieden werden:
- Python ist eine flexible Sprache. Wir schreiben wie wir wollen, je nachdem was wir wollen. Wir scheuen keine zyklischen Abhängigkeiten, Attributsubstitution für Klassen zur Laufzeit usw.
- Python ist eine spezielle Sprache. Es gibt idiomatische Möglichkeiten, Architektur und Abhängigkeiten zu erstellen. Die Datenübertragung auf und ab des Aufrufstapels wird von Iteratoren, Coroutinen und Kontextmanagern durchgeführt.
Klassenbericht zu diesem Thema und BeispielBrandon Rhodes, Dropbox:
Heben Sie Ihr IO hoch .
Beispiel aus dem Bericht:
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
- Pythons Flexibilität ist eine zusätzliche Möglichkeit, sich in den Fuß zu schießen. Sie benötigen strenge Regeln für die Verwaltung von Abhängigkeiten. Ein gutes Beispiel sind die russischen Dry-Python- Typen. Es gibt immer noch einen weniger hardcore Ansatz - Django-Struktur für Skalierbarkeit und Langlebigkeit , aber die Idee ist die gleiche.
Es gibt mehrere Artikel zum Abhängigkeitsmanagement in Python (
Beispiel 1 ,
Beispiel 2 ), die jedoch alle darauf abzielen, die Dependency Injection-Frameworks einer Person zu bewerben. Dieser Artikel ist ein neuer Eintrag zum gleichen Thema, aber diesmal ist es ein reines Gedankenexperiment ohne Werbung. Dies ist ein Versuch, ein Gleichgewicht zwischen den drei oben genannten Ansätzen zu finden, auf einen zusätzlichen Rahmen zu verzichten und ihn „pythonisch“ zu machen.
Ich habe kürzlich
Clean Architecture gelesen - und ich scheine zu verstehen, welchen Wert die Abhängigkeitsinjektion in Python hat und wie sie implementiert werden kann. Ich habe das am Beispiel meines eigenen Projekts gesehen. Kurz gesagt, dies
schützt den Code vor dem Brechen, wenn sich ein anderer Code ändert .
Ausgangsdaten
Es gibt einen API-Client, der HTTP-Anforderungen für den Service Shortener ausführt:
Und es gibt ein Modul, das alle Links im Text verkürzt. Dazu verwendet er den Shortener-API-Client:
Die Logik der Codeausführung befindet sich in einer separaten Steuerdatei (nennen wir es einen Controller):
Alles arbeitet. Der Prozessor analysiert den Text, verkürzt die Links mit einem Shortener und gibt das Ergebnis zurück. Die Abhängigkeiten sehen folgendermaßen aus:

Das Problem
Hier ist das Problem: Die
TextProcessor
Klasse hängt von der
TextProcessor
Klasse ab - und
bricht ab, wenn sich die ShortenerClient
Schnittstelle ändert .
Wie kann das passieren?
Angenommen, wir haben in unserem Projekt beschlossen,
shorten_link
zu verfolgen, und der
shorten_link
Methode das Argument
callback_url
hinzugefügt. Dieses Argument bezeichnet die Adresse, an die Benachrichtigungen gesendet werden sollen, wenn Sie auf einen Link klicken.
Die
ShortenerClient.shorten_link
Methode sah folgendermaßen aus:
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']
Und was passiert? Und es stellt sich heraus, dass wir beim Start eine Fehlermeldung erhalten:
TypeError: shorten_link() missing 1 required positional argument: 'callback_url'
Das heißt, wir haben den Shortener gewechselt, aber nicht er hat gebrochen, sondern sein Kunde:

Also was? Nun, die aufrufende Datei ist kaputt gegangen, wir haben sie repariert. Was ist das Problem?
Wenn dies in einer Minute gelöst ist - sie gingen und korrigierten -, dann ist dies natürlich überhaupt kein Problem. Wenn die Klassen wenig Code enthalten und Sie sie selbst unterstützen (dies ist Ihr Nebenprojekt, dies sind zwei kleine Klassen desselben Subsystems usw.), können Sie dort anhalten.
Probleme beginnen, wenn:
- Die aufrufenden und aufgerufenen Module haben viel Code.
- Verschiedene Module werden von verschiedenen Personen / Teams unterstützt.
Wenn Sie die
ShortenerClient
Klasse schreiben und Ihr Kollege
TextProcessor
schreibt,
TextProcessor
eine beleidigende Situation auf:
Sie haben den Code geändert, aber er ist TextProcessor
. Und es brach an einem Ort, den Sie im Leben noch nicht gesehen haben, und jetzt müssen Sie sich hinsetzen und den Code eines anderen verstehen.
Noch interessanter ist es, wenn Ihr Modul an mehreren Orten und nicht an einem verwendet wird. und Ihre Bearbeitung wird den Code auf dem Haufen von Dateien brechen.
Daher kann das Problem wie folgt formuliert werden: Wie kann der Code so organisiert werden, dass beim
ShortenerClient
der
ShortenerClient
Schnittstelle
ShortenerClient
selbst ShortenerClient
und nicht seine Verbraucher (von denen es viele geben kann)?
Die Lösung hier ist:
- Klassenkonsumenten und die Klasse selbst müssen sich auf eine gemeinsame Schnittstelle einigen. Diese Schnittstelle sollte zum Gesetz werden.
- Wenn die Klasse nicht mehr ihrer Schnittstelle entspricht, sind dies ihre Probleme und keine Verbraucherprobleme.

Frieren Sie die Schnittstelle ein
Wie sieht das Reparieren einer Schnittstelle in Python aus? Dies ist eine abstrakte Klasse:
from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key): pass @abstractmethod def shorten_link(self, link): pass
Wenn wir jetzt von dieser Klasse erben und vergessen, eine Methode zu implementieren, erhalten wir eine Fehlermeldung:
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
Das reicht aber nicht. Eine abstrakte Klasse erfasst nur die Namen von Methoden, nicht jedoch deren Signatur.
mypy
Sie ein zweites Tool zur Überprüfung der Signatur
mypy
Dieses zweite Tool ist
mypy
. Es hilft dabei, die Signaturen geerbter Methoden zu überprüfen. Dazu müssen wir der Schnittstelle Anmerkungen hinzufügen:
Wenn wir diesen Code jetzt mit
mypy
, erhalten wir aufgrund des zusätzlichen Arguments
callback_url
eine Fehlermeldung:
mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient"
Jetzt haben wir eine zuverlässige Möglichkeit, die Klassenschnittstelle festzuschreiben.
Abhängigkeitsinversion
Nachdem wir die Schnittstelle getestet haben, müssen wir sie an einen anderen Ort verschieben, um die Abhängigkeit des Verbrauchers von der Datei shortener_client.py vollständig zu beseitigen. Sie können die Benutzeroberfläche beispielsweise direkt auf den Consumer ziehen - in eine Datei mit dem
TextProcessor
Prozessor:
Und das wird die Richtung der Sucht ändern! Jetzt besitzt der
TextProcessor
die Interaktionsschnittstelle,
TextProcessor
davon abhängt und nicht umgekehrt.

Mit einfachen Worten können wir das Wesen unserer Transformation wie folgt beschreiben:
TextProcessor
sagt: Ich bin ein Prozessor und an der Textkonvertierung beteiligt. Ich möchte nichts über den Verkürzungsmechanismus wissen: Das geht mich nichts an. Ich möchte die shorten_link
Methode ziehen, damit sie alles für mich shorten_link
. Geben Sie mir bitte ein Objekt, das nach meinen Regeln spielt. Entscheidungen darüber, wie ich interagiere, werden von mir getroffen, nicht von ihm.ShortenerClient
sagt: Es scheint, dass ich nicht in einem Vakuum existieren kann und sie erfordern ein bestimmtes Verhalten von mir. Ich werde TextProcessor
fragen, was ich TextProcessor
muss, um nicht zu brechen.
Mehrere Verbraucher
Wenn mehrere Module Verkürzungslinks verwenden, sollte die Schnittstelle nicht in einer von ihnen, sondern in einer separaten Datei platziert werden, die sich über den anderen Dateien befindet und eine höhere Hierarchie aufweist:

Steuerkomponente
Wenn Verbraucher ShortenerClient nicht importieren, wer importiert es dann und erstellt ein Klassenobjekt? Es sollte eine Steuerungskomponente sein - in unserem Fall ist es
controller.py
.
Der einfachste Ansatz ist eine einfache Abhängigkeitsinjektion, die Abhängigkeitsinjektion „in die Stirn“. Wir erstellen Objekte im aufrufenden Code, übertragen ein Objekt auf ein anderes. Gewinn
Python-Ansatz
Es wird angenommen, dass ein eher „pythonischer“ Ansatz die Abhängigkeitsinjektion durch Vererbung ist.
Raymond Hettinger spricht in seinem Super-Super-Bericht ausführlich darüber. Um den Code an diesen Stil anzupassen, müssen Sie den
TextProcessor
leicht ändern, damit er vererbbar wird:
Und dann erben Sie es im aufrufenden Code:
Das zweite Beispiel ist in populären Frameworks allgegenwärtig:
- Bei Django werden wir ständig geerbt. Wir definieren Methoden der klassenbasierten Ansicht, Modelle, Formen neu. Mit anderen Worten, fügen Sie unsere Abhängigkeiten in die bereits debuggte Arbeit des Frameworks ein.
- In DRF das Gleiche. Wir erweitern Ansichten, Serializer und Berechtigungen.
- Usw. Es gibt viele Beispiele.
Das zweite Beispiel sieht hübscher und vertrauter aus, nicht wahr? Lassen Sie es uns entwickeln und sehen, ob diese Schönheit erhalten bleibt.
Python-Entwicklung
In der Geschäftslogik gibt es normalerweise mehr als zwei Komponenten. Angenommen, unser
TextProcessor
ist keine unabhängige Klasse, sondern nur eines der
TextPipeline
Elemente, die den Text verarbeiten und an die E-Mail
TextPipeline
:
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)
Wenn wir die
TextPipeline
von den verwendeten Klassen isolieren
TextPipeline
, müssen wir das gleiche Verfahren wie zuvor
TextPipeline
:
- Die
TextPipeline
Klasse deklariert Schnittstellen für die verwendeten Komponenten. - gebrauchte Komponenten müssen sich an diese Schnittstellen anpassen;
- Ein externer Code fügt alles zusammen und wird ausgeführt.
Das Abhängigkeitsdiagramm sieht folgendermaßen aus:

Aber wie sieht der Assembler-Code dieser Abhängigkeiten jetzt aus?
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()
Hast du es bemerkt? Wir erben zuerst die
TextProcessor
Klasse, um den
TextPipeline
darin einzufügen, und erben dann die
TextPipeline
, um unseren neu definierten
TextProcessor
(sowie
Mailer
) darin einzufügen. Wir haben mehrere Ebenen der sequentiellen Neudefinition. Schon kompliziert.
Warum sind alle Frameworks so organisiert?
Ja, weil es nur für Frameworks geeignet ist.- Alle Ebenen des Frameworks sind klar definiert und ihre Anzahl ist begrenzt. In Django können Sie beispielsweise
FormField
überschreiben, um es in eine Überschreibung eines Form
einzufügen, um ein Formular in eine Überschreibung von View
einzufügen. Das ist alles. Drei Ebenen. - Jedes Framework dient einem Zweck. Diese Aufgabe ist klar definiert.
- Jedes Framework verfügt über eine detaillierte Dokumentation, in der beschrieben wird, wie und was geerbt werden soll. was und mit was zu kombinieren.
Können Sie Ihre Geschäftslogik klar und eindeutig identifizieren und dokumentieren? Besonders die Architektur der Ebenen, auf denen es funktioniert? Ich nicht. Leider lässt sich der Ansatz von Raymond Hettinger nicht auf die Geschäftslogik übertragen.
Zurück zum Stirnansatz
Bei mehreren Schwierigkeitsgraden gewinnt ein einfacher Ansatz. Es sieht einfacher aus - und ist einfacher zu ändern, wenn sich die Logik ändert.
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()
Wenn jedoch die Anzahl der Logikebenen zunimmt, wird selbst ein solcher Ansatz unpraktisch. Wir müssen unbedingt eine Reihe von Klassen initiieren und sie ineinander übergehen. Ich möchte viele Verschachtelungsebenen vermeiden.
Versuchen wir noch einen Anruf.
Globaler Instanzspeicher
Versuchen wir, ein globales Wörterbuch zu erstellen, in dem die Instanzen der benötigten Komponenten liegen. Und lassen Sie diese Komponenten sich gegenseitig durch Zugriff auf dieses Wörterbuch erreichen.
Nennen wir es
INSTANCE_DICT
:
Der Trick besteht darin
, unsere Objekte in dieses Wörterbuch aufzunehmen, bevor auf sie zugegriffen wird . Dies werden wir in
controller.py
tun:
Vorteile der Arbeit mit einem globalen Wörterbuch:
- keine Motorhaubenmagie und zusätzliche DI-Rahmen;
- eine flache Liste von Abhängigkeiten, in denen Sie die Verschachtelung nicht verwalten müssen;
- Alle DI-Boni: einfaches Testen, Unabhängigkeit, Schutz der Komponenten vor Ausfällen, wenn andere Komponenten geändert werden.
Anstatt
INSTANCE_DICT
, können Sie natürlich eine Art DI-Framework verwenden. aber das Wesen davon wird sich nicht ändern. Das Framework bietet eine flexiblere Verwaltung von Instanzen. Er erlaubt Ihnen, sie in Form von Singletones oder Bündeln wie eine Fabrik zu erstellen. aber die Idee wird gleich bleiben.
Vielleicht reicht mir das irgendwann nicht mehr und ich wähle immer noch einen Rahmen.
Und vielleicht ist all dies unnötig und es ist einfacher, darauf zu verzichten: Schreiben Sie direkte Importe und erstellen Sie keine unnötigen abstrakten Schnittstellen.
Welche Erfahrungen haben Sie mit dem Abhängigkeitsmanagement in Python gemacht? Und im Allgemeinen - ist es notwendig oder erfinde ich ein Problem aus der Luft?