Verwenden Sie keine If-Else-Ketten mehr: Verwenden Sie stattdessen das Registrierungsmuster in Python


Verwenden Sie keine If-Else-Ketten mehr: Verwenden Sie stattdessen das Registrierungsmuster in Python

# Einführung

Jede Python-Codebasis hat dieses Downside. Eine Funktion, die klein anfängt. Zwei Filialen, vielleicht drei. Dann fügt jemand einen Fall hinzu, jemand anderes fügt einen weiteren hinzu, und ein Jahr später hat man 200 Zeilen davon if/elif/else das keiner anfassen will. Hier ist ein Beispiel:

def get_model(title):
    if title == "logreg":
        return LogisticRegression()
    elif title == "random_forest":
        return RandomForestClassifier()
    elif title == "svm":
        return SVC()
    elif title == "xgboost":
        return XGBClassifier()
    # ... 15 extra branches
    else:
        increase ValueError(f"Unknown mannequin: {title}")

Und ja, es funktioniert. Aber es bricht auch die Offen/Geschlossen-Prinzipdie besagt, dass Softwareeinheiten (Klassen, Module und Funktionen) für Erweiterungen offen, aber für Änderungen geschlossen sein sollten. Es gibt einen besseren Weg, dieses Downside zu lösen: den Registrierungsmuster. In diesem Artikel wird erläutert, was das Registrierungsmuster ist, wie man es von einem fünfzeiligen Wörterbuch zu einer wiederverwendbaren Klasse in Produktionsqualität aufbaut und wann es tatsächlich seinen Platz in Ihrem Code verdient. Additionally, fangen wir an.

# Das Downside mit If-Else-Ketten

Eine lange bedingte Kette schlägt auf verschiedene Arten fehl:

  • Es verstößt gegen das Offen/Geschlossen-Prinzip. Neuer Fall, neue Bearbeitung einer Funktion, die bereits funktioniert hat. Der gestern getestete Code wird aufgebrochen, erneut getestet und erneut überprüft. Die Änderungseinheit sollte „Datei hinzufügen“ und nicht „Zentralen Dispatcher ändern“ lauten.
  • Es bündelt unzusammenhängende Logik an einem Ort. Angenommen, Ihr Zahlungsdienstleister deckt Kreditkarten, PayPal und Krypto ab. Jetzt teilen sich drei Domänen, die nichts miteinander zu tun haben, eine Funktion. Der elif Die Leiter zwingt sie, sich trotzdem ein Zimmer zu teilen.
  • Es skaliert schlecht. Jeder neue Zweig erhöht das kognitive Gewicht der gesamten Funktion. Zwanzig Zweige sind zwanzig Dinge, an denen Sie jedes Mal vorbeiscrollen müssen, wenn Sie Zweig Nummer drei debuggen.
  • Eine Erweiterung von außen ist nicht möglich. Versenden Sie eine Bibliothek mit einer fest codierten Datei get_model() Kette und Ihre Benutzer stecken fest. Sie können ihr eigenes Modell nicht ohne Monkey-Patching oder Forking hinzufügen. Die Logik ist verschlossen.

Das Registrierungsmuster behebt alle vier, indem es die Beziehung umdreht. Anstatt dass der Dispatcher über jede Choice Bescheid weiß, kündigt sich jede Choice dem Dispatcher an.

Was ist das Registrierungsmuster?

Es handelt sich im Grunde um eine zentrale Nachschlagetabelle, die Schlüssel Objekten (Funktionen, Klassen, Instanzen) zuordnet, wobei sich jedes Objekt selbst registriert, anstatt in eine Bedingung fest codiert zu werden. In Python ist diese Nachschlagetabelle quick immer ein Wörterbuch, und die „Registrierung“ erfolgt normalerweise mit einem Dekorator.

# Vom If-Else zum Wörterbuch

Der kleinstmögliche Gewinn besteht darin, die Kette gegen eine Wörterbuchsuche auszutauschen. Ein Schritt, und der lineare Scan ist weg:

MODEL_REGISTRY = {
    "logreg": LogisticRegression,
    "random_forest": RandomForestClassifier,
    "svm": SVC,
    "xgboost": XGBClassifier,
}

def get_model(title):
    attempt:
        return MODEL_REGISTRY(title)
    besides KeyError:
        increase ValueError(
            f"Unknown mannequin: {title!r}. "
            f"Accessible: {listing(MODEL_REGISTRY)}"
        ) from None

Dies ist bereits ein Register – nur ein handgepflegtes. Dispatch ist O(1), die Optionen sind mit introspectable listing(MODEL_REGISTRY)und der Dispatcher ändert sich nie. Ein Wermutstropfen bleibt bestehen: Jedes neue Modell bedeutet immer noch, das Diktat zu bearbeiten und seine Klasse am Anfang der Datei zu importieren. Sie können es besser machen, indem Sie jede Komponente sich selbst registrieren lassen.

# Erstellen einer Decorator-basierten Registrierung

Dies ist die Model, die Sie tatsächlich täglich verwenden werden. Die Registrierung erfolgt in einem Dekorator, sodass jede Funktion oder Klasse ihren eigenen Schlüssel genau dort deklariert, wo er 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(quantity):
    return f"Charged ${quantity} to bank card"

@register("paypal")
def charge_paypal(quantity):
    return f"Charged ${quantity} by way of PayPal"

@register("crypto")
def charge_crypto(quantity):
    return f"Charged ${quantity} in crypto"

def process_payment(payment_type, quantity):
    handler = PAYMENT_HANDLERS.get(payment_type)
    if handler is None:
        increase ValueError(f"Unknown fee kind: {payment_type!r}")
    return handler(quantity)

Schauen Sie sich an, was sich geändert hat. Der process_payment Der Dispatcher besteht aus vier Zeilen und wird nie wachsen. Möchten Sie Apple Pay? Schreiben Sie eine neue Funktion, schlagen Sie @register("apple_pay") darauf, legen Sie es in eine beliebige Datei ab, und schon sind Sie fertig. Keine zentrale Liste zum Bearbeiten. Kein Zusammenführungskonflikt. Kein erneutes Öffnen des getesteten Codes. Der Handler sitzt direkt neben seinem eigenen Schlüssel, genau dort, wo der nächste Leser danach suchen wird.

# Erstellen einer wiederverwendbaren Registrierungsklasse

Sobald Sie zwei oder drei Register herumliegen haben, werden Sie es leid sein, immer wieder das gleiche Dekorator-Boilerplate umzuschreiben. Packen Sie es in eine kleine Klasse und Sie erhalten kostenlos eine Kollisionserkennung, bessere Fehlermeldungen und eine saubere API:

class Registry:
    """A reusable name-to-object registry."""

    def __init__(self, title):
        self.title = title
        self._registry = {}

    def register(self, key):
        def decorator(obj):
            if key in self._registry:
                increase KeyError(
                    f"{key!r} already registered in {self.title!r}"
                )
            self._registry(key) = obj
            return obj
        return decorator

    def get(self, key):
        if key not in self._registry:
            increase KeyError(
                f"{key!r} not present in {self.title!r}. "
                f"Accessible: {listing(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 nun, um eine Textverarbeitungspipeline zu erstellen, die vollständig von der Konfiguration gesteuert wird:

transforms = Registry("transforms")

@transforms.register("lowercase")
def to_lower(textual content):
    return textual content.decrease()

@transforms.register("strip")
def strip_whitespace(textual content):
    return textual content.strip()

@transforms.register("remove_digits")
def remove_digits(textual content):
    return "".be a part of(c for c in textual content if not c.isdigit())

# The pipeline is now simply knowledge. It may come from a YAML file,
# a CLI argument, or a database row.
pipeline = ("strip", "lowercase", "remove_digits")
textual content = "  Order #4521 CONFIRMED  "

for step in pipeline:
    textual content = transforms.get(step)(textual content)

print(repr(textual content))

Output:
'order # confirmed'

Hier macht sich das Muster bezahlt. Das Verhalten des Programms wird jetzt durch Daten – eine Liste von Zeichenfolgen – und nicht durch Code beschrieben. Das Neuanordnen der Pipeline, das Hinzufügen eines Schritts oder die Weitergabe des Ganzen an einen Nicht-Programmierer über eine Konfigurationsdatei wird zu einer trivialen Aufgabe.

# Klassen automatisch registrieren mit __init_subclass__

Wenn Ihre Registry Klassen anstelle von Funktionen enthält, hat Python einen noch raffinierteren Trick. Der __init_subclass__ Hook (verfügbar seit Python 3.6) wird jedes Mal automatisch ausgelöst, wenn eine Unterklasse definiert wird, sodass Unterklassen sich selbst ohne Dekorator registrieren:

class DataLoader:
    _registry = {}

    def __init_subclass__(cls, fmt=None, **kwargs):
        tremendous().__init_subclass__(**kwargs)
        if fmt:
            DataLoader._registry(fmt) = cls

    @classmethod
    def get_loader(cls, fmt):
        if fmt not in cls._registry:
            increase ValueError(
                f"No loader for {fmt!r}. "
                f"Accessible: {listing(cls._registry)}"
            )
        return cls._registry(fmt)

class CSVLoader(DataLoader, fmt="csv"):
    def load(self, path):
        return f"Loading CSV from {path}"

class JSONLoader(DataLoader, fmt="json"):
    def load(self, path):
        return f"Loading JSON from {path}"

class ParquetLoader(DataLoader, fmt="parquet"):
    def load(self, path):
        return f"Loading Parquet from {path}"

loader = DataLoader.get_loader("parquet")
print(loader.load("gross sales.parquet"))   # Loading Parquet from gross sales.parquet

Nirgendwo ein Dekorateur. Unterklassen DataLoader mit einem fmt= Das Argument reicht aus, um die neue Klasse zu registrieren. Auf diese Weise bauen viele Frameworks ihre Plugin-Systeme unter der Haube auf.

# Wo das Registrierungsmuster in der Praxis nützlich ist

Dies ist keine akademische Übung. Es ist das Rückgrat der Instruments, die Sie bereits verwenden.

  • Experimentkonfigurationen für maschinelles Lernen. Umarmende Gesichtstransformatoren, Detectron2Und MMDetection Alle verwenden Registrierungen, sodass Sie ein Modell, einen Optimierer oder eine Erweiterung anhand des Zeichenfolgennamens in einer YAML-Datei auswählen können. build_model({"mannequin": "resnet50"}) schlägt einen Riesen if spine == ... Block, und es ermöglicht Forschern, Architekturen hinzuzufügen, ohne jemals den Coach zu berühren.
  • Dateiformat und Parser-Versand. Kartenerweiterungen wie "csv", "json"Und "parquet" zu Loader-Klassen. Die Unterstützung eines neuen Codecs wird zum „Schreiben einer Klasse“ und nicht zum „Bearbeiten des Loaders“.
  • Internet-Framework-Routing. Flasche‚S @app.route("/customers") Und Klicken‚S @cli.command() sind getarnte Register. Der Dekorator ordnet der Funktion, die ihn verarbeitet, eine URL oder einen Befehlsnamen zu.
  • Plugin-Architekturen. Jedes „Legen Sie eine Datei in diesem Ordner ab und es funktioniert einfach“-System – ob pytest Vorrichtungen, Luftstrom Operatoren oder Serialisierungs-Backends – ist quick immer eine Registrierung, die Komponenten zum Zeitpunkt des Imports sammelt.
  • Occasion-Handler und Zustandsmaschinen. Ordnen Sie Ereignisnamen oder -zustände Handlerfunktionen zu, anstatt auf ihnen zu verzweigen. Die Übergangstabelle wird zu einem lesbaren Wörterbuch und nicht zu einer Ansammlung von Bedingungen.

# Praktische Überlegungen und Dinge, auf die Sie achten sollten

  • Die Registrierung erfolgt erst beim Import. Ein Dekorator wird ausgeführt, wenn Python die Datei ausführt, in der er sich befindet. Wenn Ihre Handler darin sitzen handlers/apple_pay.py und nichts importiert jemals dieses Modul, das @register Der Dekorateur feuert nie und der Handler verschwindet stillschweigend. Die Lösung besteht darin, sicherzustellen, dass Registrierungsmodule importiert werden – normalerweise durch einen expliziten Import in ein Paket __init__.pyoder eine kleine Entdeckungsschleife mit pkgutil.iter_modules das importiert alles in einen Plugin-Ordner.
  • Schutz vor stillen Überschreibungen. Bei einem einfachen Diktat schlagen sich zwei Komponenten, die denselben Schlüssel registrieren, gegenseitig, ohne einen Piepton zu hören. Als die Registry Wie die obige Klasse zeigt, verwandelt das Erhöhen eines doppelten Schlüssels einen verwirrenden Laufzeitfehler in einen offensichtlichen Fehler beim Import.
  • Zeigen Sie den Leuten, was verfügbar ist. Legen Sie die Schlüssel immer mit frei listing(registry.keys()) und fügen Sie sie in Ihre Fehlermeldungen ein. „Unbekanntes Modell: ‚lgbm‘. Verfügbar: (‘logreg‘, ‚random_forest‘, ‚xgboost‘)“ spart weitaus mehr Debugging-Zeit als ein bloßes KeyError.
  • Greifen Sie nicht zu früh danach. Für zwei oder drei stabile Zweige, deren Logik sich wirklich unterscheidet, ist eine Registrierung übertrieben. Wenn die Zweige keine gemeinsame Signatur haben oder die Bedingungen eher Bereiche als diskrete Schlüssel sind (if rating > 0.9 ... elif rating > 0.5 ...), ist eine einfache Bedingung klarer. Die Registrierung gewinnt in einer bestimmten State of affairs: Sie geben einen diskreten Schlüssel zu austauschbaren Verhaltensweisen weiter und erwarten, dass diese Verhaltensweisen wachsen.

# Zusammenfassung

Das Registrierungsmuster ist ein wachsendes, zentrales und schwer erweiterbares Muster if/elif/else Kette für eine Nachschlagetabelle, die Komponenten selbst ausfüllen. Die Auszahlung ist konkret. Ihr Dispatcher ändert sich nicht mehr. Neue Funktionen werden als neue Dateien angezeigt, statt als Änderungen an alten. Das Verhalten wird zu etwas, das Sie über eine Konfiguration steuern können. Und Benutzer Ihres Codes erhalten einen echten Erweiterungspunkt anstelle einer verschlossenen Tür.

Fangen Sie klein an. Das nächste Mal ertappen Sie sich dabei, wie Sie ein Drittel tippen elif title == ...halte inne und frage, ob ein Wörterbuch reichen würde. Normalerweise würde es so sein. Von dort sind die Dekorator- und Klassenversionen nur einen Katzensprung entfernt.

# Earlier than
if variety == "a": ...
elif variety == "b": ...
elif variety == "c": ...

# After
@registry.register("a")
def handle_a(): ...

Ihr zukünftiges Ich, das an einem Dispatcher mit vier Leitungen statt an einer Leiter mit 200 Leitungen vorbeiscrollt, wird es Ihnen danken.

Kanwal Mehreen ist ein Ingenieur für maschinelles Lernen und ein technischer Redakteur mit einer großen Leidenschaft für Datenwissenschaft und die Schnittstelle zwischen KI und Medizin. Sie ist Mitautorin des E-Books „Maximizing Productiveness with ChatGPT“. Als Google Technology Scholar 2022 für APAC setzt sie sich für Vielfalt und akademische Exzellenz ein. Sie ist außerdem als Teradata Variety in Tech Scholar, Mitacs Globalink Analysis Scholar und Harvard WeCode Scholar anerkannt. Kanwal ist ein leidenschaftlicher Verfechter von Veränderungen und hat FEMCodes gegründet, um Frauen in MINT-Bereichen zu stärken.

Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert