Die Verwendung von langen If-Else-Ketten in Python führt oft zu unübersichtlichem und schwer wartbarem Code.
In vielen Python-Projekten beginnt eine Funktion oft klein, mit nur wenigen Verzweigungen. Doch mit der Zeit, wenn immer mehr Fälle hinzugefügt werden, kann der Code auf über 200 Zeilen anwachsen, was die Wartung erschwert. Ein typisches Beispiel könnte so aussehen:
def get_model(name):
if name == "logreg":
return LogisticRegression()
elif name == "random_forest":
return RandomForestClassifier()
elif name == "svm":
return SVC()
elif name == "xgboost":
return XGBClassifier()
# ... 15 weitere Verzweigungen
else:
raise ValueError(f"Unbekanntes Modell: {name}")
Obwohl dieser Ansatz funktioniert, verstößt er gegen das Open/Closed-Prinzip, das besagt, dass Softwareeinheiten (wie Klassen, Module und Funktionen) für Erweiterungen offen, aber für Modifikationen geschlossen sein sollten. Eine bessere Lösung für dieses Problem ist das Registry-Pattern. In diesem Artikel wird erläutert, was das Registry-Pattern ist, wie man es von einem einfachen Wörterbuch zu einer produktionsreifen, wiederverwendbaren Klasse aufbaut und wann es sinnvoll ist, es in den eigenen Code zu integrieren.
Die Probleme mit If-Else-Ketten
Eine lange bedingte Kette hat mehrere spezifische Nachteile:
- Sie verstößt gegen das Open/Closed-Prinzip. Bei jedem neuen Fall muss eine bereits funktionierende Funktion bearbeitet werden. Der getestete Code von gestern wird erneut geöffnet, getestet und überprüft.
- Sie bündelt nicht zusammenhängende Logik an einem Ort. Wenn beispielsweise ein Zahlungsabwickler Kreditkarten, PayPal und Kryptowährungen abdeckt, teilen sich drei völlig unterschiedliche Bereiche eine Funktion.
- Sie skaliert schlecht. Jede neue Verzweigung erhöht die kognitive Last der gesamten Funktion. Bei zwanzig Verzweigungen muss man zwanzig Dinge durchscrollen, wenn man die dritte Verzweigung debuggt.
- Sie kann nicht von außen erweitert werden. Wenn eine Bibliothek mit einer fest kodierten get_model()-Kette ausgeliefert wird, sind die Benutzer eingeschränkt. Sie können ihr eigenes Modell nicht hinzufügen, ohne den Code zu ändern oder eine Abspaltung vorzunehmen.
Das Registry-Pattern löst all diese Probleme, indem es die Beziehung umkehrt. Anstatt dass der Dispatcher über jede Option Bescheid weiß, meldet sich jede Option beim Dispatcher an.
Was ist das Registry-Pattern?
Es handelt sich im Wesentlichen um eine zentrale Nachschlagetabelle, die Schlüssel mit Objekten (Funktionen, Klassen, Instanzen) verknüpft, wobei jedes Objekt sich selbst registriert, anstatt in einer bedingten Anweisung fest kodiert zu sein. In Python ist diese Nachschlagetabelle fast immer ein Wörterbuch, und das „Registrieren“ erfolgt in der Regel mit einem Dekorator.
Von If-Else zu einem Wörterbuch
Der einfachste Schritt besteht darin, die Kette durch eine Wörterbuchabfrage zu ersetzen. Ein Schritt, und die lineare Suche entfällt:
MODEL_REGISTRY = {
"logreg": LogisticRegression,
"random_forest": RandomForestClassifier,
"svm": SVC,
"xgboost": XGBClassifier,
}
def get_model(name):
try:
return MODEL_REGISTRY[name]
except KeyError:
raise ValueError(
f"Unbekanntes Modell: {name!r}. "
f"Verfügbar: {list(MODEL_REGISTRY)}"
) from None
Dies ist bereits ein Registry — nur ein manuell gepflegtes. Der Dispatch ist O(1), die Optionen sind mit list(MODEL_REGISTRY) einsehbar, und der Dispatcher ändert sich nie. Ein Nachteil bleibt: Jedes neue Modell erfordert immer noch eine Bearbeitung des Wörterbuchs und das Importieren seiner Klasse am Anfang der Datei. Es ist möglich, dies zu verbessern, indem man es jedem Bestandteil ermöglicht, sich selbst zu registrieren.
Erstellung eines Dekorator-basierten Registrierungsansatzes
Dies ist die Version, die im Alltag tatsächlich verwendet wird. Die Registrierung erfolgt in einem Dekorator, sodass jede Funktion oder Klasse ihren eigenen Schlüssel direkt dort deklariert, wo sie definiert ist:
PAYMENT_HANDLERS = {}
def register(payment_type):
def decorator(func):
PAYMENT_HANDLERS[payment_type] = func
return func
return decorator
@register("credit_card")
def charge_credit_card(amount):
return f"Charged ${amount} to credit card"
@register("paypal")
def charge_paypal(amount):
return f"Charged ${amount} via PayPal"
@register("crypto")
def charge_crypto(amount):
return f"Charged ${amount} in crypto"
def process_payment(payment_type, amount):
handler = PAYMENT_HANDLERS.get(payment_type)
if handler is None:
raise ValueError(f"Unbekannter Zahlungstyp: {payment_type!r}")
return handler(amount)
Betrachten Sie, was sich geändert hat. Der Dispatcher process_payment umfasst nur vier Zeilen und wird nie wachsen. Möchten Sie Apple Pay? Schreiben Sie eine neue Funktion, fügen Sie @register(„apple_pay“) hinzu, platzieren Sie sie in der gewünschten Datei, und das war’s. Keine zentrale Liste zum Bearbeiten. Keine Merge-Konflikte. Kein erneutes Öffnen getesteter Codes. Der Handler befindet sich direkt neben seinem eigenen Schlüssel, was genau der Ort ist, an dem der nächste Leser danach suchen wird.
Erstellung einer wiederverwendbaren Registrierungs-Klasse
Sobald Sie zwei oder drei Registrierungen haben, werden Sie es leid, denselben Dekorator-Basiscode immer wieder zu schreiben. Wickeln Sie es in eine kleine Klasse ein, und Sie erhalten Kollisionserkennung, bessere Fehlermeldungen und eine saubere API kostenlos:
class Registry:
"""Eine wiederverwendbare Namens-zu-Objekt-Registrierung."""
def __init__(self, name):
self.name = name
self._registry = {}
def register(self, key):
def decorator(obj):
if key in self._registry:
raise KeyError(
f"{key!r} bereits in {self.name!r} registriert"
)
self._registry[key] = obj
return obj
return decorator
def get(self, key):
if key not in self._registry:
raise KeyError(
f"{key!r} nicht gefunden in {self.name!r}. "
f"Verfügbar: {list(self._registry)}"
)
return self._registry[key]
def __contains__(self, key):
return key in self._registry
def keys(self):
return self._registry.keys()
Verwenden Sie es, um eine textverarbeitende Pipeline zu erstellen, die vollständig von der Konfiguration gesteuert wird:
transforms = Registry("transforms")
@transforms.register("lowercase")
def to_lower(text):
return text.lower()
@transforms.register("strip")
def strip_whitespace(text):
return text.strip()
@transforms.register("remove_digits")
def remove_digits(text):
return "".join(c for c in text if not c.isdigit())
# Die Pipeline ist jetzt nur noch Daten. Sie könnte aus einer YAML-Datei,
# einem CLI-Argument oder einer Datenbankzeile stammen.
pipeline = ["strip", "lowercase", "remove_digits"]
text = " Order #4521 CONFIRMED "
for step in pipeline:
text = transforms.get(step)(text)
print(repr(text))
Ausgabe:
‚order # confirmed‘
Hier zeigt sich der Vorteil des Musters. Das Verhalten des Programms wird jetzt durch Daten — eine Liste von Zeichenfolgen — und nicht durch Code beschrieben. Das Umordnen der Pipeline, das Hinzufügen eines Schrittes oder das Übergeben des gesamten Prozesses an einen Nicht-Programmierer über eine Konfigurationsdatei wird dadurch trivial.
Automatische Registrierung von Klassen mit __init_subclass__
Wenn Ihr Registry Klassen anstelle von Funktionen enthält, hat Python einen noch eleganteren Trick. Der __init_subclass__-Hook (verfügbar seit Python 3.6) wird automatisch jedes Mal ausgelöst, wenn eine Unterklasse definiert wird, sodass sich Unterklassen ohne Dekorator registrieren:
class DataLoader:
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
super().__init_subclass__(**kwargs)
if fmt:
DataLoader._registry[fmt] = cls
@classmethod
def get_loader(cls, fmt):
if fmt not in cls._registry:
raise ValueError(
f"Kein Loader für {fmt!r}. "
f"Verfügbar: {list(cls._registry)}"
)
return cls._registry[fmt]
class CSVLoader(DataLoader, fmt="csv"):
def load(self, path):
return f"Lade CSV von {path}"
class JSONLoader(DataLoader, fmt="json"):
def load(self, path):
return f"Lade JSON von {path}"
class ParquetLoader(DataLoader, fmt="parquet"):
def load(self, path):
return f"Lade Parquet von {path}"
loader = DataLoader.get_loader("parquet")
print(loader.load("sales.parquet")) # Lade Parquet von sales.parquet
Kein Dekorator erforderlich. Das Erstellen einer Unterklasse von DataLoader mit einem fmt=-Argument reicht aus, um die neue Klasse zu registrieren. So bauen viele Frameworks ihre Plug-in-Systeme im Hintergrund auf.
Wo das Registry-Pattern in der Praxis nützlich ist
Dies ist keine akademische Übung. Es ist das Rückgrat von Werkzeugen, die bereits verwendet werden:
- Konfigurationen für maschinelles Lernen: Hugging Face Transformers, Detectron2 und MMDetection verwenden alle Registrierungen, sodass Sie ein Modell, einen Optimierer oder eine Augmentation anhand des Namens in einer YAML-Datei auswählen können.
- Dateiformat- und Parser-Dispatch: Verknüpfen Sie Erweiterungen wie „csv“, „json“ und „parquet“ mit Loader-Klassen.
- Routing in Web-Frameworks: Flask’s @app.route(„/users“) und Click’s @cli.command() sind Registrierungen in Verkleidung.
- Plugin-Architekturen: Systeme, die „eine Datei in diesen Ordner legen und es funktioniert einfach“ — sei es pytest-Fixierungen, Airflow-Operatoren oder Serializer-Backends — sind fast immer eine Registrierung, die Komponenten zur Importzeit sammelt.
- Event-Handler und Zustandsmaschinen: Verknüpfen Sie Ereignisnamen oder Zustände mit Handler-Funktionen, anstatt sie zu verzweigen.
Praktische Überlegungen und Dinge, auf die man achten sollte
Die Registrierung erfolgt nur beim Import. Ein Dekorator wird ausgeführt, wenn Python die Datei ausführt, in der er sich befindet. Wenn Ihre Handler in handlers/apple_pay.py sitzen und dieses Modul nie importiert wird, wird der @register-Dekorator niemals ausgeführt, und der Handler bleibt stillschweigend verschwunden. Die Lösung besteht darin, sicherzustellen, dass Registrierungs-Module importiert werden — normalerweise durch einen expliziten Import in der __init__.py einer Bibliothek oder eine kleine Entdeckungs-Schleife mit pkgutil.iter_modules, die alles in einem Plugin-Ordner importiert.
Schützen Sie sich vor stillen Überschreibungen. Bei einem einfachen Wörterbuch überschreiben zwei Komponenten, die denselben Schlüssel registrieren, einander ohne eine Warnung. Wie die oben gezeigte Registry-Klasse zeigt, verwandelt das Anheben bei einem doppelten Schlüssel einen verwirrenden Laufzeitfehler in einen offensichtlichen Fehler zur Importzeit.
Zeigen Sie den Benutzern, was verfügbar ist. Stellen Sie immer die Schlüssel mit list(registry.keys()) zur Verfügung und fügen Sie sie in Ihre Fehlermeldungen ein. „Unbekanntes Modell: ‚lgbm‘. Verfügbar: [‚logreg‘, ‚random_forest‘, ‚xgboost‘]“ spart viel mehr Debugging-Zeit als ein einfaches KeyError.
Greifen Sie nicht zu früh darauf zurück. Ein Registry ist übertrieben für zwei oder drei stabile Verzweigungen, deren Logik sich wirklich unterscheidet. Wenn die Verzweigungen keine gemeinsame Signatur teilen oder die Bedingungen Bereiche anstelle von diskreten Schlüsseln sind, ist eine einfache Bedingung klarer. Das Registry-Pattern gewinnt in einer spezifischen Situation: Wenn Sie auf einem diskreten Schlüssel zu austauschbaren Verhaltensweisen dispatchen und erwarten, dass sich diese Verhaltensweisen erweitern.
Zusammenfassung
Das Registry-Pattern tauscht eine wachsende, zentrale, schwer zu erweiternde If/Elif/Else-Kette gegen eine Nachschlagetabelle aus, die von den Komponenten selbst ausgefüllt wird. Der Vorteil ist konkret. Ihr Dispatcher hört auf, sich zu ändern. Neue Funktionen erscheinen als neue Dateien anstelle von Änderungen an alten. Verhalten wird zu etwas, das Sie aus einer Konfiguration steuern können. Und die Benutzer Ihres Codes erhalten einen echten Erweiterungspunkt anstelle einer verschlossenen Tür.
Beginnen Sie klein. Wenn Sie das nächste Mal dabei sind, eine dritte elif-Bedingung zu tippen, halten Sie inne und fragen Sie sich, ob ein Wörterbuch ausreichen würde. In der Regel wird es das. Von dort aus sind die Dekorator- und Klassenversionen nur einen kurzen Sprung entfernt.
Vorher:
if kind == „a“: …
elif kind == „b“: …
elif kind == „c“: …
Nachher:
@registry.register(„a“)
def handle_a(): …
Ihr zukünftiges Ich, das an einem vierzeiligen Dispatcher vorbeiscrollt, anstatt an einer 200-zeiligen Leiter, wird es Ihnen danken.
Kanwal Mehreen ist Maschinenbauingenieurin und technische Autorin mit einer tiefen Leidenschaft für Datenwissenschaft und der Schnittstelle von KI mit der Medizin. Sie hat das E-Book „Maximizing Productivity with ChatGPT“ mitverfasst. Als Google Generation Scholar 2022 für APAC setzt sie sich für Vielfalt und akademische Exzellenz ein. Sie wurde auch als Teradata Diversity in Tech Scholar, Mitacs Globalink Research Scholar und Harvard WeCode Scholar anerkannt. Kanwal ist eine leidenschaftliche Verfechterin des Wandels und hat FEMCodes gegründet, um Frauen in MINT-Berufen zu stärken. In ihren Arbeiten beschäftigt sie sich auch mit der Frage, wie die Struktur von Datensätzen den Programmierstil beeinflusst und hat 5 Powerful Python Decorators to Optimize LLM Applications veröffentlicht.
„`
Quellen: kdnuggets
Bildquelle: KI generiert
🚀