Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 636 – Strukturelles Pattern Matching: Tutorial

Autor:
Daniel F Moisset <dfmoisset at gmail.com>
Sponsor:
Guido van Rossum <guido at python.org>
BDFL-Delegate:

Discussions-To:
Python-Dev Liste
Status:
Final
Typ:
Informational
Erstellt:
12. Sep. 2020
Python-Version:
3.10
Post-History:
22. Okt. 2020, 08. Feb. 2021
Resolution:
Nachricht der Python-Committers

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP ist ein Tutorial für das Mustervergleich (Pattern Matching), das durch PEP 634 eingeführt wurde.

PEP 622 schlug eine Syntax für das Mustervergleich vor, die sowohl von der Community als auch vom Steering Council eingehend diskutiert wurde. Eine häufige Sorge war, wie einfach es sein würde, dieses Feature zu erklären (und zu lernen). Dieses PEP adressiert diese Sorge, indem es eine Art von Dokumentation bereitstellt, die Entwickler zum Erlernen des Mustervergleichs in Python verwenden können.

Dies gilt als unterstützendes Material für PEP 634 (die technische Spezifikation für Mustervergleich) und PEP 635 (die Motivation und Begründung für Mustervergleich und Designüberlegungen).

Für Leser, die eher eine schnelle Übersicht als ein Tutorial suchen, siehe Anhang A.

Tutorial

Als Beispiel zur Motivation dieses Tutorials schreiben wir ein Text-Adventure. Das ist eine Form von interaktiver Fiktion, bei der der Benutzer Textbefehle eingibt, um mit einer fiktiven Welt zu interagieren und Textbeschreibungen dessen erhält, was passiert. Befehle werden vereinfachte Formen natürlicher Sprache sein, wie z.B. get sword, attack dragon, go north, enter shop oder buy cheese.

Sequenzen abgleichen

Ihre Hauptschleife muss Eingaben vom Benutzer erhalten und diese in Wörter aufteilen, sagen wir eine Liste von Zeichenketten wie diese

command = input("What are you doing next? ")
# analyze the result of command.split()

Der nächste Schritt ist die Interpretation der Wörter. Die meisten unserer Befehle werden zwei Wörter haben: eine Aktion und ein Objekt. Sie könnten also versucht sein, Folgendes zu tun

[action, obj] = command.split()
... # interpret action, obj

Das Problem mit dieser Codezeile ist, dass etwas fehlt: Was passiert, wenn der Benutzer mehr oder weniger als 2 Wörter eingibt? Um dieses Problem zu vermeiden, können Sie entweder die Länge der Wortliste überprüfen oder den ValueError abfangen, den die obige Anweisung auslösen würde.

Sie können stattdessen eine Match-Anweisung verwenden

match command.split():
    case [action, obj]:
        ... # interpret action, obj

Die Match-Anweisung wertet das **"Subjekt"** (den Wert nach dem Schlüsselwort match) aus und vergleicht es mit dem **Muster** (dem Code neben case). Ein Muster kann zwei verschiedene Dinge tun

  • Überprüfen, ob das Subjekt eine bestimmte Struktur hat. In Ihrem Fall gleicht das Muster [action, obj] jede Sequenz von genau zwei Elementen ab. Dies wird als **Matching** bezeichnet.
  • Es werden einige Namen im Muster an die Komponentenelemente Ihres Subjekts gebunden. In diesem Fall, wenn die Liste zwei Elemente hat, wird action = subject[0] und obj = subject[1] gebunden.

Wenn es eine Übereinstimmung gibt, werden die Anweisungen im Case-Block mit den gebundenen Variablen ausgeführt. Wenn keine Übereinstimmung gefunden wird, passiert nichts und die Anweisung nach match wird als nächstes ausgeführt.

Beachten Sie, dass Sie ähnlich wie bei Entpackungszuweisungen Klammern, eckige Klammern oder nur eine Komma-Trennung als Synonyme verwenden können. Sie könnten also case action, obj oder case (action, obj) mit derselben Bedeutung schreiben. Alle Formen passen zu jeder Sequenz (z. B. Listen oder Tupel).

Mehrere Muster abgleichen

Auch wenn die meisten Befehle die Aktion/Objekt-Form haben, möchten Sie vielleicht Befehle unterschiedlicher Länge haben. Sie möchten zum Beispiel einzelne Verben ohne Objekt wie look oder quit hinzufügen. Eine Match-Anweisung kann (und wird wahrscheinlich) mehr als einen case haben.

match command.split():
    case [action]:
        ... # interpret single-verb action
    case [action, obj]:
        ... # interpret action, obj

Die Match-Anweisung prüft Muster von oben nach unten. Wenn das Muster nicht zum Subjekt passt, wird das nächste Muster ausprobiert. Sobald jedoch das *erste* passende Muster gefunden wurde, wird der Körper dieses Falls ausgeführt und alle weiteren Fälle werden ignoriert. Dies ähnelt der Funktionsweise einer if/elif/elif/...-Anweisung.

Spezifische Werte abgleichen

Ihr Code muss immer noch die spezifischen Aktionen betrachten und bedingt unterschiedliche Logik ausführen, abhängig von der spezifischen Aktion (z. B. quit, attack oder buy). Sie könnten dies mit einer Kette von if/elif/elif/... tun oder mit einem Dictionary von Funktionen, aber hier werden wir den Mustervergleich nutzen, um diese Aufgabe zu lösen. Anstelle einer Variablen können Sie literale Werte in Mustern verwenden (wie "quit", 42 oder None). Dies ermöglicht Ihnen Folgendes zu schreiben:

match command.split():
    case ["quit"]:
        print("Goodbye!")
        quit_game()
    case ["look"]:
        current_room.describe()
    case ["get", obj]:
        character.get(obj, current_room)
    case ["go", direction]:
        current_room = current_room.neighbor(direction)
    # The rest of your commands go here

Ein Muster wie ["get", obj] passt nur zu 2-elementigen Sequenzen, deren erstes Element gleich "get" ist. Es bindet auch obj = subject[1].

Wie Sie im go-Fall sehen können, können wir in verschiedenen Mustern auch unterschiedliche Variablennamen verwenden.

Literale Werte werden mit dem == Operator verglichen, mit Ausnahme der Konstanten True, False und None, die mit dem is Operator verglichen werden.

Mehrere Werte abgleichen

Ein Spieler kann möglicherweise mehrere Gegenstände fallen lassen, indem er eine Reihe von Befehlen verwendet: drop key, drop sword, drop cheese. Diese Schnittstelle kann umständlich sein, und Sie möchten möglicherweise das Fallenlassen mehrerer Gegenstände in einem einzigen Befehl zulassen, wie z.B. drop key sword cheese. In diesem Fall wissen Sie im Voraus nicht, wie viele Wörter der Befehl enthalten wird, aber Sie können erweiterte Entpackungsmuster verwenden, auf die gleiche Weise, wie sie bei Zuweisungen erlaubt sind.

match command.split():
    case ["drop", *objects]:
        for obj in objects:
            character.drop(obj, current_room)
    # The rest of your commands go here

Dies passt zu jeder Sequenz, die "drop" als erstes Element hat. Alle verbleibenden Elemente werden in einem list-Objekt erfasst, das an die Variable objects gebunden wird.

Diese Syntax hat ähnliche Einschränkungen wie die Sequenzentpackung: Sie können nicht mehr als einen Sternchen-Namen in einem Muster haben.

Wildcard hinzufügen

Sie möchten vielleicht eine Fehlermeldung ausgeben, die besagt, dass der Befehl nicht erkannt wurde, wenn alle Muster fehlschlagen. Sie könnten die gerade erlernte Funktion nutzen und case [*ignored_words] als Ihr letztes Muster schreiben. Es gibt jedoch einen viel einfacheren Weg.

match command.split():
    case ["quit"]: ... # Code omitted for brevity
    case ["go", direction]: ...
    case ["drop", *objects]: ...
    ... # Other cases
    case _:
        print(f"Sorry, I couldn't understand {command!r}")

Dieses spezielle Muster, das als _ geschrieben wird (und Wildcard genannt wird), passt immer, bindet aber keine Variablen.

Beachten Sie, dass dies mit jedem Objekt übereinstimmt, nicht nur mit Sequenzen. Daher ist es nur sinnvoll, es allein als letztes Muster zu haben (um Fehler zu vermeiden, wird Python Sie daran hindern, es vorher zu verwenden).

Muster zusammensetzen

Dies ist ein guter Zeitpunkt, um von den Beispielen zurückzutreten und zu verstehen, wie die Muster, die Sie verwendet haben, aufgebaut sind. Muster können ineinander verschachtelt werden, und wir haben dies in den obigen Beispielen implizit getan.

Es gibt einige "einfache" Muster ("einfach" bedeutet hier, dass sie keine anderen Muster enthalten), die wir gesehen haben:

  • Capture-Muster (eigenständige Namen wie direction, action, objects). Wir haben diese nie separat diskutiert, sondern sie als Teil anderer Muster verwendet.
  • Literal-Muster (Zeichenkettenliterale, Zahlenliterale, True, False und None)
  • Die Wildcard-Muster _

Bis jetzt ist das einzige nicht-einfache Muster, mit dem wir experimentiert haben, das Sequenzmuster. Jedes Element in einem Sequenzmuster kann tatsächlich jedes andere Muster sein. Das bedeutet, dass Sie ein Muster wie ["first", (left, right), _, *rest] schreiben könnten. Dies passt zu Subjekten, die eine Sequenz von mindestens drei Elementen sind, wobei das erste gleich "first" ist und das zweite wiederum eine Sequenz von zwei Elementen ist. Es bindet auch left=subject[1][0], right=subject[1][1] und rest = subject[3:].

Oder-Muster

Zurück zum Adventure-Spiel-Beispiel: Möglicherweise möchten Sie, dass mehrere Muster zum gleichen Ergebnis führen. Sie möchten zum Beispiel, dass die Befehle north und go north äquivalent sind. Möglicherweise möchten Sie auch Aliase für get X, pick up X und pick X up für jedes X haben.

Das Symbol | in Mustern kombiniert sie als Alternativen. Sie könnten zum Beispiel schreiben

match command.split():
    ... # Other cases
    case ["north"] | ["go", "north"]:
        current_room = current_room.neighbor("north")
    case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]:
        ... # Code for picking up the given object

Dies wird als **Or-Muster** bezeichnet und liefert das erwartete Ergebnis. Muster werden von links nach rechts ausprobiert; dies kann relevant sein, um zu wissen, was gebunden wird, wenn mehr als eine Alternative passt. Eine wichtige Einschränkung beim Schreiben von Or-Mustern ist, dass alle Alternativen dieselben Variablen binden sollten. Ein Muster wie [1, x] | [2, y] ist nicht erlaubt, da es unklar macht, welche Variable nach einer erfolgreichen Übereinstimmung gebunden würde. [1, x] | [2, x] ist vollkommen in Ordnung und bindet x immer, wenn es erfolgreich ist.

Abgeglichene Unter-Muster erfassen

Die erste Version unseres "go"-Befehls wurde mit dem Muster ["go", direction] geschrieben. Die Änderung, die wir in unserer letzten Version mit dem Muster ["north"] | ["go", "north"] vorgenommen haben, hat einige Vorteile, aber auch einige Nachteile im Vergleich: Die neueste Version erlaubt den Alias, hat aber auch die Richtung fest codiert, was uns zwingen wird, tatsächlich separate Muster für Norden/Süden/Osten/Westen zu haben. Dies führt zu einer gewissen Code-Duplizierung, aber gleichzeitig erhalten wir eine bessere Eingabevalidierung, und wir werden nicht in diesen Zweig gelangen, wenn der vom Benutzer eingegebene Befehl "go figure!" anstelle einer Richtung ist.

Wir könnten versuchen, das Beste aus beiden Welten zu bekommen, indem wir Folgendes tun (ich lasse die Aliase-Version ohne "go" zur Kürze weg)

match command.split():
    case ["go", ("north" | "south" | "east" | "west")]:
        current_room = current_room.neighbor(...)
        # how do I know which direction to go?

Dieser Code ist ein einzelner Zweig und überprüft, ob das Wort nach "go" tatsächlich eine Richtung ist. Aber der Code, der den Spieler bewegt, muss wissen, welche Richtung gewählt wurde, und hat keine Möglichkeit, dies zu erfahren. Was wir brauchen, ist ein Muster, das sich wie das Or-Muster verhält, aber gleichzeitig eine Erfassung vornimmt. Das können wir mit einem **As-Muster** erreichen.

match command.split():
    case ["go", ("north" | "south" | "east" | "west") as direction]:
        current_room = current_room.neighbor(direction)

Das As-Muster passt zu jedem Muster auf seiner linken Seite, bindet aber auch den Wert an einen Namen.

Bedingungen zu Mustern hinzufügen

Die oben untersuchten Muster können mächtige Datenfilterung durchführen, aber manchmal wünschen Sie sich die volle Kraft eines booleschen Ausdrucks. Nehmen wir an, Sie möchten tatsächlich einen "go"-Befehl nur in einer eingeschränkten Auswahl von Richtungen zulassen, basierend auf den möglichen Ausgängen aus dem aktuellen Raum. Dies können wir erreichen, indem wir unserem Case einen **Guard** hinzufügen. Guards bestehen aus dem Schlüsselwort if, gefolgt von einem beliebigen Ausdruck.

match command.split():
    case ["go", direction] if direction in current_room.exits:
        current_room = current_room.neighbor(direction)
    case ["go", _]:
        print("Sorry, you can't go that way")

Der Guard ist kein Teil des Musters, er ist Teil des Cases. Er wird nur geprüft, wenn das Muster passt, und nachdem alle Muster-Variablen gebunden wurden (deshalb kann die Bedingung die Variable direction im obigen Beispiel verwenden). Wenn das Muster passt und die Bedingung wahr ist, wird der Körper des Cases normal ausgeführt. Wenn das Muster passt, aber die Bedingung falsch ist, fährt die Match-Anweisung fort, den nächsten Fall zu prüfen, als ob das Muster nicht gepasst hätte (mit dem möglichen Nebeneffekt, dass bereits einige Variablen gebunden wurden).

Eine Benutzeroberfläche hinzufügen: Objekte abgleichen

Ihr Abenteuer wird ein Erfolg und Sie wurden gebeten, eine grafische Benutzeroberfläche zu implementieren. Ihr bevorzugtes UI-Toolkit ermöglicht es Ihnen, eine Ereignisschleife zu schreiben, in der Sie ein neues Ereignisobjekt erhalten können, indem Sie event.get() aufrufen. Das resultierende Objekt kann je nach Benutzeraktion unterschiedlichen Typ und Attribute haben, zum Beispiel:

  • Ein KeyPress-Objekt wird generiert, wenn der Benutzer eine Taste drückt. Es hat ein key_name-Attribut mit dem Namen der gedrückten Taste und einige andere Attribute bezüglich Modifikatoren.
  • Ein Click-Objekt wird generiert, wenn der Benutzer die Maustaste klickt. Es hat ein Attribut position mit den Koordinaten des Zeigers.
  • Ein Quit-Objekt wird generiert, wenn der Benutzer auf die Schließen-Schaltfläche des Spielfensters klickt.

Anstatt mehrere isinstance()-Prüfungen zu schreiben, können Sie Muster verwenden, um verschiedene Arten von Objekten zu erkennen und auch Muster auf deren Attribute anzuwenden.

match event.get():
    case Click(position=(x, y)):
        handle_click_at(x, y)
    case KeyPress(key_name="Q") | Quit():
        game.quit()
    case KeyPress(key_name="up arrow"):
        game.go_north()
    ...
    case KeyPress():
        pass # Ignore other keystrokes
    case other_event:
        raise ValueError(f"Unrecognized event: {other_event}")

Ein Muster wie Click(position=(x, y)) passt nur, wenn der Typ des Ereignisses eine Unterklasse der Click-Klasse ist. Es verlangt außerdem, dass das Ereignis ein position-Attribut hat, das dem (x, y)-Muster entspricht. Wenn es eine Übereinstimmung gibt, erhalten die lokalen Variablen x und y die erwarteten Werte.

Ein Muster wie KeyPress(), ohne Argumente, passt zu jedem Objekt, das eine Instanz der KeyPress-Klasse ist. Nur die in Ihrem Muster angegebenen Attribute werden abgeglichen, und alle anderen Attribute werden ignoriert.

Positionsattribute abgleichen

Der vorherige Abschnitt beschrieb, wie benannte Attribute beim Objektvergleich abgeglichen werden. Für einige Objekte kann es praktisch sein, die abgeglichenen Argumente nach Position zu beschreiben (insbesondere wenn es nur wenige Attribute gibt und diese eine "Standard"-Reihenfolge haben). Wenn die von Ihnen verwendeten Klassen benannte Tupel oder Dataclasses sind, können Sie dies tun, indem Sie die gleiche Reihenfolge befolgen, die Sie bei der Erstellung eines Objekts verwenden würden. Wenn beispielsweise das obige UI-Framework seine Klasse wie folgt definiert:

from dataclasses import dataclass

@dataclass
class Click:
    position: tuple
    button: Button

dann können Sie Ihre obige Match-Anweisung wie folgt umschreiben:

match event.get():
    case Click((x, y)):
        handle_click_at(x, y)

Das Muster (x, y) wird automatisch gegen das Attribut position abgeglichen, da das erste Argument im Muster dem ersten Attribut in Ihrer Dataclass-Definition entspricht.

Andere Klassen haben keine natürliche Reihenfolge ihrer Attribute, daher müssen Sie explizite Namen in Ihrem Muster verwenden, um mit ihren Attributen abzugleichen. Es ist jedoch möglich, die Reihenfolge der Attribute manuell anzugeben, was den Positionsabgleich ermöglicht, wie in dieser alternativen Definition:

class Click:
    __match_args__ = ("position", "button")
    def __init__(self, pos, btn):
        self.position = pos
        self.button = btn
        ...

Das spezielle Attribut __match_args__ definiert eine explizite Reihenfolge für Ihre Attribute, die in Mustern wie case Click((x,y)) verwendet werden kann.

Abgleich mit Konstanten und Enums

Ihr obiges Muster behandelt alle Maustasten gleich, und Sie haben beschlossen, Links-Klicks zu akzeptieren und andere Tasten zu ignorieren. Dabei stellen Sie fest, dass das Attribut button als Button typisiert ist, was eine Enumeration ist, die mit enum.Enum erstellt wurde. Sie können tatsächlich mit Enum-Werten wie folgt abgleichen:

match event.get():
    case Click((x, y), button=Button.LEFT):  # This is a left click
        handle_click_at(x, y)
    case Click():
        pass  # ignore other clicks

Dies funktioniert mit jedem Punktnamen (wie math.pi). Ein nicht qualifizierter Name (d. h. ein nackter Name ohne Punkte) wird jedoch immer als Capture-Muster interpretiert. Vermeiden Sie daher diese Mehrdeutigkeit, indem Sie in Mustern immer qualifizierte Konstanten verwenden.

Ab in die Cloud: Mappings

Sie haben beschlossen, eine Online-Version Ihres Spiels zu erstellen. Ihre gesamte Logik befindet sich auf einem Server, und die Benutzeroberfläche auf einem Client, der über JSON-Nachrichten kommuniziert. Über das json-Modul werden diese auf Python-Dictionaries, Listen und andere eingebaute Objekte abgebildet.

Unser Client empfängt eine Liste von Dictionaries (geparst aus JSON) von Aktionen, die ausgeführt werden sollen, wobei jedes Element beispielsweise wie folgt aussieht:

  • {"text": "Der Ladenbesitzer sagt 'Ah! Wir haben Camembert, ja mein Herr'", "color": "blue"}
  • Wenn der Client eine Pause einlegen soll {"sleep": 3}
  • Um einen Ton abzuspielen {"sound": "filename.ogg", "format": "ogg"}

Bis jetzt haben unsere Muster Sequenzen verarbeitet, aber es gibt Muster, um Mappings anhand ihrer vorhandenen Schlüssel abzugleichen. In diesem Fall könnten Sie verwenden

for action in actions:
    match action:
        case {"text": message, "color": c}:
            ui.set_text_color(c)
            ui.display(message)
        case {"sleep": duration}:
            ui.wait(duration)
        case {"sound": url, "format": "ogg"}:
            ui.play(url)
        case {"sound": _, "format": _}:
            warning("Unsupported audio format")

Die Schlüssel in Ihrem Mapping-Muster müssen Literale sein, aber die Werte können beliebige Muster sein. Wie bei Sequenzmustern müssen alle Submuster übereinstimmen, damit das allgemeine Muster übereinstimmt.

Sie können **rest in einem Mapping-Muster verwenden, um zusätzliche Schlüssel im Subjekt zu erfassen. Beachten Sie, dass, wenn Sie dies weglassen, zusätzliche Schlüssel im Subjekt beim Abgleichen ignoriert werden, d. h. die Nachricht {"text": "foo", "color": "red", "style": "bold"} passt zum ersten Muster im obigen Beispiel.

Eingebaute Klassen abgleichen

Der obige Code könnte eine Validierung vertragen. Da Nachrichten aus externen Quellen stammen, könnten die Typen der Felder falsch sein, was zu Fehlern oder Sicherheitsproblemen führen könnte.

Jede Klasse ist ein gültiges Abgleichziel, und das schließt eingebaute Klassen wie bool, str oder int ein. Das erlaubt uns, den obigen Code mit einem Klassenmuster zu kombinieren. Anstatt also {"text": message, "color": c} zu schreiben, können wir {"text": str() as message, "color": str() as c} verwenden, um sicherzustellen, dass message und c beides Zeichenketten sind. Für viele eingebaute Klassen (siehe PEP 634 für die vollständige Liste) können Sie einen Positions-Parameter als Abkürzung verwenden, indem Sie str(c) anstelle von str() as c schreiben. Die vollständig umgeschriebene Version sieht wie folgt aus:

for action in actions:
    match action:
        case {"text": str(message), "color": str(c)}:
            ui.set_text_color(c)
            ui.display(message)
        case {"sleep": float(duration)}:
            ui.wait(duration)
        case {"sound": str(url), "format": "ogg"}:
            ui.play(url)
        case {"sound": _, "format": _}:
            warning("Unsupported audio format")

Anhang A – Schnelle Einführung

Eine Match-Anweisung nimmt einen Ausdruck und vergleicht seinen Wert mit aufeinanderfolgenden Mustern, die in einem oder mehreren Case-Blöcken angegeben sind. Dies ähnelt oberflächlich einer Switch-Anweisung in C, Java oder JavaScript (und vielen anderen Sprachen), ist aber wesentlich leistungsfähiger.

Die einfachste Form vergleicht einen Subjektwert mit einem oder mehreren Literalen.

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the Internet"

Beachten Sie den letzten Block: Der "Variablenname" _ fungiert als **Wildcard** und schlägt nie fehl.

Sie können mehrere Literale in einem einzigen Muster mit | ("oder") kombinieren.

case 401 | 403 | 404:
    return "Not allowed"

Muster können wie Entpackungszuweisungen aussehen und zum Binden von Variablen verwendet werden.

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

Studieren Sie das sorgfältig! Das erste Muster hat zwei Literale und kann als Erweiterung des obigen Literal-Musters betrachtet werden. Aber die nächsten beiden Muster kombinieren ein Literal und eine Variable, und die Variable **bindet** einen Wert aus dem Subjekt (point). Das vierte Muster erfasst zwei Werte, was es konzeptionell der Entpackungszuweisung (x, y) = point ähnlich macht.

Wenn Sie Klassen zur Strukturierung Ihrer Daten verwenden, können Sie den Klassennamen gefolgt von einer Argumentenliste verwenden, die einem Konstruktor ähnelt, aber mit der Fähigkeit, Attribute in Variablen zu erfassen.

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

Sie können Positions-Parameter mit einigen eingebauten Klassen verwenden, die eine Reihenfolge für ihre Attribute bereitstellen (z. B. Dataclasses). Sie können auch eine bestimmte Position für Attribute in Mustern definieren, indem Sie das spezielle Attribut __match_args__ in Ihren Klassen festlegen. Wenn es auf ("x", "y") gesetzt ist, sind die folgenden Muster alle äquivalent (und binden alle das Attribut y an die Variable var):

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

Muster können beliebig verschachtelt werden. Zum Beispiel, wenn wir eine kurze Liste von Punkten haben, könnten wir sie so abgleichen:

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

Wir können eine if-Klausel zu einem Muster hinzufügen, bekannt als "Guard". Wenn der Guard falsch ist, versucht match, den nächsten Case-Block zu versuchen. Beachten Sie, dass die Wert-Erfassung vor der Auswertung des Guards erfolgt.

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

Mehrere andere Schlüsselfunktionen

  • Wie Entpackungszuweisungen haben Tupel- und Listenmuster exakt dieselbe Bedeutung und passen tatsächlich zu beliebigen Sequenzen. Eine wichtige Ausnahme ist, dass sie keine Iteratoren oder Zeichenketten abgleichen. (Technisch muss das Subjekt eine Instanz von collections.abc.Sequence sein.)
  • Sequenzmuster unterstützen Wildcards: [x, y, *rest] und (x, y, *rest) funktionieren ähnlich wie Wildcards bei Entpackungszuweisungen. Der Name nach * kann auch _ sein, sodass (x, y, *_) eine Sequenz von mindestens zwei Elementen abgleicht, ohne die verbleibenden Elemente zu binden.
  • Mapping-Muster: {"bandwidth": b, "latency": l} erfasst die Werte "bandwidth" und "latency" aus einem Dict. Im Gegensatz zu Sequenzmustern werden zusätzliche Schlüssel ignoriert. Eine Wildcard **rest wird ebenfalls unterstützt. (Aber **_ wäre redundant und daher nicht erlaubt.)
  • Submuster können mit dem Schlüsselwort as erfasst werden.
    case (Point(x1, y1), Point(x2, y2) as p2): ...
    
  • Die meisten Literale werden nach Gleichheit verglichen, jedoch werden die Singletons True, False und None nach Identität verglichen.
  • Muster können benannte Konstanten verwenden. Dies müssen Punktnamen sein, um zu verhindern, dass sie als Capture-Variablen interpretiert werden.
    from enum import Enum
    class Color(Enum):
        RED = 0
        GREEN = 1
        BLUE = 2
    
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")
    

Quelle: https://github.com/python/peps/blob/main/peps/pep-0636.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT