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

Python Enhancement Proposals

PEP 622 – Strukturelles Pattern Matching

Autor:
Brandt Bucher <brandt at python.org>, Daniel F Moisset <dfmoisset at gmail.com>, Tobias Kohn <kohnt at tobiaskohn.ch>, Ivan Levkivskyi <levkivskyi at gmail.com>, Guido van Rossum <guido at python.org>, Talin <viridia at gmail.com>
BDFL-Delegate:

Discussions-To:
Python-Dev Liste
Status:
Abgelöst
Typ:
Standards Track
Erstellt:
23. Jun 2020
Python-Version:
3.10
Post-History:
23. Jun 2020, 08. Jul 2020
Ersetzt-Durch:
634

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP schlägt vor, eine Pattern Matching Anweisung zu Python hinzuzufügen, inspiriert von ähnlicher Syntax in Scala, Erlang und anderen Sprachen.

Muster und Formen

Die Pattern-Syntax baut auf Pythons bestehender Syntax für Sequenz-Entpackung auf (z.B. a, b = value).

Eine match-Anweisung vergleicht einen Wert (das Subjekt) mit mehreren verschiedenen Formen (den Mustern), bis eine Form passt. Jedes Muster beschreibt den Typ und die Struktur der akzeptierten Werte sowie die Variablen, in denen deren Inhalt erfasst werden soll.

Muster können die Form angeben, die

  • eine zu entpackende Sequenz ist, wie bereits erwähnt
  • ein Mapping mit spezifischen Schlüsseln ist
  • eine Instanz einer gegebenen Klasse mit (optional) spezifischen Attributen ist
  • ein spezifischer Wert ist
  • ein Platzhalter ist

Muster können auf verschiedene Arten kombiniert werden.

Syntax

Syntaktisch enthält eine match-Anweisung

  • einen *Subjekt*-Ausdruck
  • eine oder mehrere case-Klauseln

Jede case-Klausel gibt an

  • ein Muster (die Gesamtform, die abgeglichen werden soll)
  • eine optionale "Guard" (eine Bedingung, die geprüft werden soll, wenn das Muster passt)
  • einen Codeblock, der ausgeführt werden soll, wenn die Case-Klausel ausgewählt wird

Motivation

Der Rest des PEP

  • motiviert, warum wir glauben, dass Pattern Matching eine gute Ergänzung für Python darstellt
  • erklärt unsere Designentscheidungen
  • enthält eine präzise syntaktische und Laufzeit-Spezifikation
  • gibt Anleitungen für statische Typenprüfer (und eine kleine Ergänzung zum typing-Modul)
  • diskutiert die Haupteinwände und Alternativen, die während ausführlicher Diskussionen des Vorschlags sowohl innerhalb der Autorengruppe als auch in der python-dev-Community aufkamen

Schließlich diskutieren wir einige mögliche Erweiterungen, die in Zukunft in Betracht gezogen werden könnten, sobald die Community reichlich Erfahrung mit der derzeit vorgeschlagenen Syntax und Semantik gesammelt hat.

Übersicht

Muster sind eine neue syntaktische Kategorie mit eigenen Regeln und Sonderfällen. Muster vermischen Eingaben (gegebene Werte) und Ausgaben (erfasste Variablen) auf neuartige Weise. Es kann einige Zeit dauern, sie effektiv zu nutzen. Die Autoren haben hier eine kurze Einführung in die Grundkonzepte gegeben. Beachten Sie, dass dieser Abschnitt nicht vollständig oder vollständig korrekt sein soll.

Pattern, ein neues syntaktisches Konstrukt, und Destrukturierung

Ein neues syntaktisches Konstrukt namens Pattern wird in diesem PEP eingeführt. Syntaktisch sehen Muster wie eine Teilmenge von Ausdrücken aus. Die folgenden sind Beispiele für Muster

  • [first, second, *rest]
  • Point2d(x, 0)
  • {"name": "Bruce", "age": age}
  • 42

Die obigen Ausdrücke mögen wie Beispiele für Objektkonstruktion mit einem Konstruktor aussehen, der einige Werte als Parameter nimmt und ein Objekt aus diesen Komponenten erstellt.

Wenn sie als Muster betrachtet werden, bedeuten die obigen Muster die umgekehrte Operation der Konstruktion, die wir Destrukturierung nennen. Destrukturierung nimmt einen Subjektwert und extrahiert seine Komponenten.

Die syntaktische Ähnlichkeit zwischen Objektkonstruktion und Destrukturierung ist beabsichtigt. Sie folgt auch dem bestehenden Python-Stil von Kontexten, die Zuweisungsziele (Schreibkontexte) wie Ausdrücke (Lesekontexte) aussehen lassen.

Pattern Matching erstellt niemals Objekte. Dies geschieht auf die gleiche Weise, wie [a, b] = my_list keine neue [a, b]-Liste erstellt und auch nicht die Werte von a und b liest.

Matching-Prozess

Während dieses Matching-Prozesses passt die Struktur des Musters möglicherweise nicht zum Subjekt, und das Matching schlägt fehl.

Zum Beispiel passt das Muster Point2d(x, 0) erfolgreich zum Subjekt Point2d(3, 0). Der Match bindet auch die freie Variable x des Musters an den Wert 3 des Subjekts.

Als weiteres Beispiel schlägt der Match fehl, wenn das Subjekt [3, 0] ist, da der Typ list des Subjekts nicht mit dem Point2d des Musters übereinstimmt.

Als drittes Beispiel schlägt der Match fehl, wenn das Subjekt Point2d(3, 7) ist, da die zweite Koordinate 7 des Subjekts nicht mit der 0 des Musters übereinstimmt.

Die match-Anweisung versucht, ein einzelnes Subjekt mit jedem der Muster in ihren case-Klauseln abzugleichen. Beim ersten erfolgreichen Match mit einem Muster in einer case-Klausel

  • werden die Variablen im Muster zugewiesen, und
  • ein entsprechender Block wird ausgeführt.

Jede case-Klausel kann auch eine optionale boolesche Bedingung angeben, die als Guard bekannt ist.

Betrachten wir ein detaillierteres Beispiel für eine match-Anweisung. Die match-Anweisung wird innerhalb einer Funktion verwendet, um den Aufbau von 3D-Punkten zu definieren. In diesem Beispiel kann die Funktion jede der folgenden Eingaben akzeptieren: ein Tupel mit 2 Elementen, ein Tupel mit 3 Elementen, ein vorhandenes Point2d-Objekt oder ein vorhandenes Point3d-Objekt.

def make_point_3d(pt):
    match pt:
        case (x, y):
            return Point3d(x, y, 0)
        case (x, y, z):
            return Point3d(x, y, z)
        case Point2d(x, y):
            return Point3d(x, y, 0)
        case Point3d(_, _, _):
            return pt
        case _:
            raise TypeError("not a point we support")

Ohne Pattern Matching müsste die Implementierung dieser Funktion mehrere isinstance()-Prüfungen, einen oder zwei len()-Aufrufe und einen komplizierteren Kontrollfluss erfordern. Die match-Beispielversion und die traditionelle Python-Version ohne match übersetzen intern in ähnlichen Code. Mit Vertrautheit mit Pattern Matching wird ein Benutzer, der diese Funktion mit match liest, diese Version wahrscheinlich klarer finden als den traditionellen Ansatz.

Begründung und Ziele

Python-Programme müssen häufig Daten verarbeiten, die sich in Typ, Vorhandensein von Attributen/Schlüsseln oder Anzahl der Elemente unterscheiden. Typische Beispiele sind die Arbeit mit Knoten einer gemischten Struktur wie einem AST, die Verarbeitung von UI-Ereignissen unterschiedlicher Typen, die Verarbeitung strukturierter Eingaben (wie strukturierter Dateien oder Netzwerk-Nachrichten) oder das "Parsen" von Argumenten für eine Funktion, die unterschiedliche Kombinationen von Typen und Anzahlen von Parametern akzeptieren kann. Tatsächlich ist das klassische "Visitor"-Muster ein Beispiel dafür, das im OOP-Stil durchgeführt wird – aber mit Matching ist es viel weniger mühsam zu schreiben.

Ein Großteil des Codes dafür besteht aus komplexen Ketten von verschachtelten if/elif-Anweisungen, einschließlich mehrerer Aufrufe von len(), isinstance() und Index-/Schlüssel-/Attributzugriff. Innerhalb dieser Zweige müssen Benutzer manchmal die Daten weiter zerlegen, um die benötigten Komponentenwerte zu extrahieren, die mehrere Objekte tief verschachtelt sein können.

Pattern Matching, wie es in vielen anderen Sprachen vorkommt, bietet eine elegante Lösung für dieses Problem. Diese reichen von statisch kompilierten funktionalen Sprachen wie F# und Haskell über gemischte Paradigma-Sprachen wie Scala und Rust bis hin zu dynamischen Sprachen wie Elixir und Ruby, und wird für JavaScript in Betracht gezogen. Wir sind diesen Sprachen dankbar, dass sie uns den Weg zum Python-Pattern-Matching gewiesen haben, so wie Python vielen anderen Sprachen für viele seiner Funktionen dankbar ist: viele grundlegende syntaktische Merkmale wurden von C geerbt, Ausnahmen von Modula-3, Klassen wurden von C++ inspiriert, Slicing kam von Icon, reguläre Ausdrücke von Perl, Dekoratoren ähneln Java-Annotationen und so weiter.

Die übliche Logik für die Arbeit mit heterogenen Daten lässt sich wie folgt zusammenfassen

  • Es wird eine Analyse der Form (Typ und Komponenten) der Daten durchgeführt: Dies könnte isinstance()- oder len()-Aufrufe und/oder die Extraktion von Komponenten (über Indizierung oder Attributzugriff) umfassen, die auf spezifische Werte oder Bedingungen geprüft werden.
  • Wenn die Form den Erwartungen entspricht, werden möglicherweise weitere Komponenten extrahiert und mit den extrahierten Werten eine Operation durchgeführt.

Nehmen wir zum Beispiel dieses Stück des Django Web Frameworks

if (
    isinstance(value, (list, tuple)) and
    len(value) > 1 and
    isinstance(value[-1], (Promise, str))
):
    *value, label = value
    value = tuple(value)
else:
    label = key.replace('_', ' ').title()

Wir sehen die Form-Analyse des value am Anfang, gefolgt von der Destrukturierung im Inneren.

Beachten Sie, dass die Form-Analyse hier die Überprüfung der Typen sowohl des Containers als auch einer seiner Komponenten sowie einige Prüfungen der Anzahl der Elemente beinhaltet. Sobald wir die Form abgeglichen haben, müssen wir die Sequenz zerlegen. Mit dem Vorschlag in diesem PEP könnten wir diesen Code wie folgt umschreiben

match value:
    case [*v, label := (Promise() | str())] if v:
        value = tuple(v)
    case _:
        label = key.replace('_', ' ').title()

Diese Syntax macht expliziter, welche Formate für die Eingabedaten möglich sind und welche Komponenten von wo extrahiert werden. Sie sehen ein Muster, das Listen-Entpackung ähnelt, aber auch Typ-Prüfung: das Muster Promise() ist keine Objektkonstruktion, sondern repräsentiert alles, was eine Instanz von Promise ist. Der Musteroperator | trennt alternative Muster (nicht unähnlich regulären Ausdrücken oder EBNF-Grammatiken), und _ ist ein Platzhalter. (Beachten Sie, dass die hier verwendete Match-Syntax benutzerdefinierte Sequenzen sowie Listen und Tupel akzeptiert.)

In einigen Fällen ist die Extraktion von Informationen nicht so relevant wie die Identifizierung der Struktur. Nehmen Sie das folgende Beispiel aus der Python-Standardbibliothek

def is_tuple(node):
    if isinstance(node, Node) and node.children == [LParen(), RParen()]:
        return True
    return (isinstance(node, Node)
            and len(node.children) == 3
            and isinstance(node.children[0], Leaf)
            and isinstance(node.children[1], Node)
            and isinstance(node.children[2], Leaf)
            and node.children[0].value == "("
            and node.children[2].value == ")")

Dieses Beispiel zeigt ein Beispiel dafür, wie die "Form" der Daten ermittelt wird, ohne wesentliche Extraktionen vorzunehmen. Dieser Code ist nicht sehr leicht zu lesen, und die beabsichtigte Form, die hier abgeglichen werden soll, ist nicht ersichtlich. Vergleichen Sie dies mit dem aktualisierten Code, der die vorgeschlagene Syntax verwendet.

def is_tuple(node: Node) -> bool:
    match node:
        case Node(children=[LParen(), RParen()]):
            return True
        case Node(children=[Leaf(value="("), Node(), Leaf(value=")")]):
            return True
        case _:
            return False

Beachten Sie, dass der vorgeschlagene Code ohne Änderungen an der Definition von Node und anderen hier genannten Klassen funktioniert. Wie in den obigen Beispielen gezeigt, unterstützt der Vorschlag nicht nur das Entpacken von Sequenzen, sondern auch isinstance-Prüfungen (wie LParen() oder str()), das Betrachten von Objektattributen (Leaf(value="(") zum Beispiel) und Vergleiche mit Literalen.

Diese letzte Funktion hilft bei einigen Arten von Code, die mehr dem "switch"-Statement ähneln, wie es in anderen Sprachen vorhanden ist.

match response.status:
    case 200:
        do_something(response.data)  # OK
    case 301 | 302:
        retry(response.location)  # Redirect
    case 401:
        retry(auth=get_credentials())  # Login first
    case 426:
        sleep(DELAY)  # Server is swamped, try after a bit
        retry()
    case _:
        raise RequestError("we couldn't get the data")

Obwohl dies funktionieren wird, ist es nicht unbedingt das, worauf sich der Vorschlag konzentriert, und die neue Syntax wurde entwickelt, um die Destrukturierungsszenarien am besten zu unterstützen.

Siehe die Abschnitte Syntax unten für eine detailliertere Spezifikation.

Wir schlagen vor, dass das Entpacken von Objekten durch ein neues spezielles Attribut __match_args__ angepasst werden kann. Als Teil dieses PEP spezifizieren wir die allgemeine API und ihre Implementierung für einige Standardbibliotheksklassen (einschließlich benannter Tupel und Dataclasses). Siehe den Abschnitt Laufzeit unten.

Schließlich streben wir eine umfassende Unterstützung für statische Typenprüfer und ähnliche Werkzeuge an. Zu diesem Zweck schlagen wir die Einführung eines Klassen-Dekorators @typing.sealed vor, der zur Laufzeit keine Wirkung hat, aber statischen Werkzeugen anzeigt, dass alle Unterklassen dieser Klasse im selben Modul definiert sein müssen. Dies ermöglicht effektive statische Exhaustivitätsprüfungen und bietet zusammen mit Dataclasses grundlegende Unterstützung für algebraische Datentypen. Siehe den Abschnitt Statische Prüfer für weitere Details.

Syntax und Semantik

Muster

Das Pattern ist ein neues syntaktisches Konstrukt, das als lose Verallgemeinerung von Zuweisungszielen betrachtet werden könnte. Die Schlüsseleigenschaften eines Musters sind, welche Typen und Formen von Subjekten es akzeptiert, welche Variablen es erfasst und wie es diese aus dem Subjekt extrahiert. Zum Beispiel passt das Muster [a, b] nur zu Sequenzen von genau 2 Elementen und extrahiert das erste Element in a und das zweite in b.

Dieses PEP definiert verschiedene Arten von Mustern. Dies sind sicherlich nicht die einzigen möglichen, daher wurde die Designentscheidung getroffen, eine Teilmenge von Funktionalität zu wählen, die jetzt nützlich, aber konservativ ist. Weitere Muster können später hinzugefügt werden, sobald diese Funktion weiter verbreitet ist. Siehe die Abschnitte abgelehnte Ideen und zurückgestellte Ideen für weitere Details.

Die hier aufgeführten Muster werden unten detaillierter beschrieben, aber zu diesem Zweck der Einfachheit halber zusammengefasst

  • Ein Literalmuster ist nützlich, um konstante Werte in einer Struktur zu filtern. Es sieht aus wie ein Python-Literal (einschließlich einiger Werte wie True, False und None). Es passt nur zu Objekten, die dem Literal gleich sind, und bindet niemals.
  • Ein Erfassungsmuster sieht aus wie x und entspricht einem identischen Zuweisungsziel: es passt immer und bindet die Variable mit dem gegebenen (einfachen) Namen.
  • Das Platzhaltermuster ist ein einzelner Unterstrich: _. Es passt immer, erfasst aber keine Variable (was Interferenzen mit anderen Verwendungen von _ verhindert und einige Optimierungen ermöglicht).
  • Ein Muster für konstante Werte funktioniert wie das Literal, aber für bestimmte benannte Konstanten. Beachten Sie, dass es sich um einen qualifizierten (gepunkteten) Namen handeln muss, angesichts der möglichen Mehrdeutigkeit mit einem Erfassungsmuster. Es sieht aus wie Color.RED und passt nur zu Werten, die dem entsprechenden Wert gleich sind. Es bindet niemals.
  • Ein Sequenzmuster sieht aus wie [a, *rest, b] und ähnelt dem Listen-Entpacken. Ein wichtiger Unterschied ist, dass die darin verschachtelten Elemente beliebige Muster sein können, nicht nur Namen oder Sequenzen. Es passt nur zu Sequenzen geeigneter Länge, solange alle Teilmuster ebenfalls passen. Es macht alle Bindungen seiner Teilmuster.
  • Ein Mapping-Muster sieht aus wie {"user": u, "emails": [*es]}. Es passt zu Mappings mit mindestens der Menge der bereitgestellten Schlüssel und wenn alle Teilmuster zu ihren entsprechenden Werten passen. Es bindet, was die Teilmuster binden, während es mit den Werten übereinstimmt, die den Schlüsseln entsprechen. Das Hinzufügen von **rest am Ende des Musters, um zusätzliche Elemente zu erfassen, ist erlaubt.
  • Ein Klassenmuster ähnelt dem obigen, passt aber zu Attributen anstelle von Schlüsseln. Es sieht aus wie datetime.date(year=y, day=d). Es passt zu Instanzen des gegebenen Typs, die mindestens die spezifizierten Attribute haben, solange die Attribute mit den entsprechenden Teilmustern übereinstimmen. Es bindet, was die Teilmuster beim Abgleich mit den Werten der gegebenen Attribute binden. Ein optionales Protokoll ermöglicht auch den Abgleich von Positionsargumenten.
  • Ein ODER-Muster sieht aus wie [*x] | {"elems": [*x]}. Es passt, wenn eines seiner Teilmuster passt. Es verwendet die Bindung des am weitesten links stehenden Musters, das gepasst hat.
  • Ein Walross-Muster sieht aus wie d := datetime(year=2020, month=m). Es passt nur, wenn sein Teilmuster ebenfalls passt. Es bindet, was der Teilmuster-Match tut, und bindet auch die benannte Variable an das gesamte Objekt.

Die match-Anweisung

Eine vereinfachte, ungefähre Grammatik für die vorgeschlagene Syntax lautet

...
compound_statement:
    | if_stmt
    ...
    | match_stmt
match_stmt: "match" expression ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" pattern [guard] ':' block
guard: 'if' expression
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: closed_pattern ('|' closed_pattern)*
closed_pattern:
    | literal_pattern
    | capture_pattern
    | wildcard_pattern
    | constant_pattern
    | sequence_pattern
    | mapping_pattern
    | class_pattern

Siehe Anhang A für die vollständige, ungekürzte Grammatik. Die vereinfachten Grammatiken in diesem Abschnitt dienen der Hilfe für den Leser und nicht als vollständige Spezifikation.

Wir schlagen vor, dass die Match-Operation eine Anweisung und keine Expression sein soll. Obwohl sie in vielen Sprachen eine Expression ist, passt die Tatsache, dass sie eine Anweisung ist, besser zur allgemeinen Logik der Python-Syntax. Siehe abgelehnte Ideen für weitere Diskussionen. Die zulässigen Muster werden unten im Abschnitt Muster detailliert beschrieben.

Die Schlüsselwörter match und case werden als "Soft Keywords" vorgeschlagen, sodass sie am Anfang einer Match-Anweisung oder eines Case-Blocks jeweils als Schlüsselwörter erkannt werden, aber an anderen Stellen als Variablen- oder Argumentnamen verwendet werden dürfen.

Die vorgeschlagene Einrückungsstruktur ist wie folgt

match some_expression:
    case pattern_1:
        ...
    case pattern_2:
        ...

Hier stellt some_expression den Wert dar, gegen den abgeglichen wird, und wird im Folgenden als das Subjekt des Matches bezeichnet.

Match-Semantik

Die vorgeschlagene groß angelegte Semantik für die Auswahl des Matches ist, das erste passende Muster auszuwählen und die entsprechende Suite auszuführen. Die restlichen Muster werden nicht ausprobiert. Wenn keine Muster passen, fällt die Anweisung "durch", und die Ausführung wird bei der folgenden Anweisung fortgesetzt.

Im Wesentlichen ist dies äquivalent zu einer Kette von if ... elif ... else-Anweisungen. Beachten Sie, dass im Gegensatz zur zuvor vorgeschlagenen switch-Anweisung die Semantik der vordefinierten Dispatch-Tabelle hier nicht zur Anwendung kommt.

Es gibt keine default- oder else-Klausel – stattdessen kann der spezielle Platzhalter _ (siehe Abschnitt Erfassungsmuster) als letzte "Catch-all"-Muster verwendet werden.

Namensbindungen, die während eines erfolgreichen Pattern Matches erstellt werden, überdauern die ausgeführte Suite und können nach der Match-Anweisung verwendet werden. Dies folgt der Logik anderer Python-Anweisungen, die Namen binden können, wie z.B. for-Schleifen und with-Anweisungen. Zum Beispiel

match shape:
    case Point(x, y):
        ...
    case Rectangle(x, y, _, _):
        ...
print(x, y)  # This works

Während fehlgeschlagener Pattern Matches können einige Teilmuster erfolgreich sein. Zum Beispiel kann während des Abgleichs des Werts [0, 1, 2] mit dem Muster (0, x, 1) das Teilmuster x erfolgreich sein, wenn die Listenelemente von links nach rechts abgeglichen werden. Die Implementierung kann wählen, ob sie persistente Bindungen für diese Teilübereinstimmungen herstellt oder nicht. Benutzercode, der eine match-Anweisung enthält, sollte sich nicht auf die für einen fehlgeschlagenen Match erstellten Bindungen verlassen, sollte aber auch nicht davon ausgehen, dass Variablen durch einen fehlgeschlagenen Match unverändert bleiben. Dieser Teil des Verhaltens ist absichtlich nicht spezifiziert, damit verschiedene Implementierungen Optimierungen hinzufügen können, und um semantische Einschränkungen zu vermeiden, die die Erweiterbarkeit dieser Funktion einschränken könnten.

Beachten Sie, dass einige Muster-Typen unten spezifischere Regeln definieren, wann die Bindung hergestellt wird.

Zulässige Muster

Wir führen die vorgeschlagene Syntax schrittweise ein. Hier beginnen wir mit den Hauptbausteinen. Die folgenden Muster werden unterstützt

Literale Muster

Vereinfachte Syntax

literal_pattern:
    | number
    | string
    | 'None'
    | 'True'
    | 'False'

Ein Literalmuster besteht aus einem einfachen Literal wie einem String, einer Zahl, einem booleschen Literal (True oder False) oder None

match number:
    case 0:
        print("Nothing")
    case 1:
        print("Just one")
    case 2:
        print("A couple")
    case -1:
        print("One less than nothing")
    case 1-1j:
        print("Good luck with that...")

Literalmuster verwenden Gleichheit mit dem Literal auf der rechten Seite, sodass im obigen Beispiel number == 0 und dann möglicherweise number == 1 usw. ausgewertet wird. Beachten Sie, dass negative Zahlen, obwohl technisch gesehen mit unärem Minus dargestellt, für die Zwecke des Pattern Matching als Literale gelten. Unäres Plus ist nicht erlaubt. Binäres Plus und Minus sind nur erlaubt, um eine reelle Zahl und eine imaginäre Zahl zu einer komplexen Zahl zu verbinden, wie z.B. 1+1j.

Beachten Sie, dass aufgrund der Verwendung von Gleichheit (__eq__) und der Äquivalenz zwischen Booleans und den ganzen Zahlen 0 und 1, praktisch kein Unterschied zwischen den folgenden beiden besteht

case True:
    ...

case 1:
    ...

Dreifach-geklammerte Strings werden unterstützt. Roh-Strings und Byte-Strings werden unterstützt. F-Strings sind nicht erlaubt (da sie im Allgemeinen keine echten Literale sind).

Erfassungsmuster

Vereinfachte Syntax

capture_pattern: NAME

Ein Erfassungsmuster dient als Zuweisungsziel für den abgeglichenen Ausdruck

match greeting:
    case "":
        print("Hello!")
    case name:
        print(f"Hi {name}!")

Es ist nur ein einzelner Name erlaubt (ein gepunkteter Name ist ein Muster für konstante Werte). Ein Erfassungsmuster schlägt niemals fehl. Ein Erfassungsmuster, das in einem Geltungsbereich erscheint, macht den Namen lokal für diesen Geltungsbereich. Zum Beispiel kann die Verwendung von name nach dem obigen Snippet UnboundLocalError anstelle von NameError auslösen, wenn die ""-Case-Klausel genommen wurde.

match greeting:
    case "":
        print("Hello!")
    case name:
        print(f"Hi {name}!")
if name == "Santa":      # <-- might raise UnboundLocalError
    ...                  # but works fine if greeting was not empty

Beim Abgleich mit jeder Case-Klausel darf ein Name höchstens einmal gebunden werden; zwei Erfassungsmuster mit übereinstimmenden Namen sind ein Fehler.

match data:
    case [x, x]:  # Error!
        ...

Hinweis: Man kann immer noch auf eine Sammlung mit gleichen Elementen mithilfe von Guards matchen. Auch [x, y] | Point(x, y) ist ein gültiges Muster, da die beiden Alternativen niemals gleichzeitig abgeglichen werden.

Der einzelne Unterstrich (_) wird nicht als NAME betrachtet und speziell als Platzhaltermuster behandelt.

Zur Erinnerung: None, False und True sind Schlüsselwörter, die Literale bezeichnen, keine Namen.

Wildcard-Muster

Vereinfachte Syntax

wildcard_pattern: "_"

Der einzelne Unterstrich (_) ist eine spezielle Art von Muster, das immer passt, aber niemals bindet.

match data:
    case [_, _]:
        print("Some pair")
        print(_)  # Error!

Da keine Bindung stattfindet, kann es beliebig oft verwendet werden, im Gegensatz zu Erfassungsmustern.

Muster für konstante Werte

Vereinfachte Syntax

constant_pattern: NAME ('.' NAME)+

Dies wird zum Abgleichen von Konstanten und Enum-Werten verwendet. Jeder gepunktete Name in einem Muster wird gemäß den normalen Python-Namensauflösungsregeln nachgeschlagen, und der Wert wird für den Gleichheitsvergleich mit dem Match-Subjekt verwendet (wie bei Literalen).

from enum import Enum

class Sides(str, Enum):
    SPAM = "Spam"
    EGGS = "eggs"
    ...

match entree[-1]:
    case Sides.SPAM:  # Compares entree[-1] == Sides.SPAM.
        response = "Have you got anything without Spam?"
    case side:  # Assigns side = entree[-1].
        response = f"Well, could I have their Spam instead of the {side} then?"

Beachten Sie, dass es keine Möglichkeit gibt, nicht qualifizierte Namen als Muster für konstante Werte zu verwenden (sie bezeichnen immer zu erfassende Variablen). Siehe abgelehnte Ideen für andere syntaktische Alternativen, die für Muster für konstante Werte in Betracht gezogen wurden.

Sequenzmuster

Vereinfachte Syntax

sequence_pattern:
    | '[' [values_pattern] ']'
    | '(' [value_pattern ',' [values pattern]] ')'
values_pattern: ','.value_pattern+ ','?
value_pattern: '*' capture_pattern | pattern

Ein Sequenzmuster folgt derselben Semantik wie das Entpacken von Zuweisungen. Wie beim Entpacken von Zuweisungen können sowohl Tupel-ähnliche als auch Listen-ähnliche Syntax verwendet werden, mit identischer Semantik. Jedes Element kann ein beliebiges Muster sein; es kann höchstens ein *name-Muster geben, um alle verbleibenden Elemente zu erfassen.

match collection:
    case 1, [x, *others]:
        print("Got 1 and a nested sequence")
    case (1, x):
        print(f"Got 1 and {x}")

Um ein Sequenzmuster abzugleichen, muss das Subjekt eine Instanz von collections.abc.Sequence sein, und es darf keine Art von String sein (str, bytes, bytearray). Es darf kein Iterator sein. Für den Abgleich einer bestimmten Collection-Klasse siehe Klassenmuster unten.

Der _-Platzhalter kann sternförmig sein, um Sequenzen unterschiedlicher Länge abzugleichen. Zum Beispiel

  • [*_] passt zu einer Sequenz beliebiger Länge.
  • (_, _, *_) passt zu jeder Sequenz der Länge zwei oder mehr.
  • ["a", *_, "z"] passt zu jeder Sequenz der Länge zwei oder mehr, die mit "a" beginnt und mit "z" endet.

Abgleichmuster

Vereinfachte Syntax

mapping_pattern: '{' [items_pattern] '}'
items_pattern: ','.key_value_pattern+ ','?
key_value_pattern:
    | (literal_pattern | constant_pattern) ':' or_pattern
    | '**' capture_pattern

Das Mapping-Muster ist eine Verallgemeinerung des Iterable-Unpackings auf Mappings. Seine Syntax ähnelt der Dictionary-Anzeige, aber jeder Schlüssel und Wert sind Muster "{" (pattern ":" pattern)+ "}". Ein **rest-Muster ist ebenfalls erlaubt, um die verbleibenden Elemente zu extrahieren. Nur Literal- und Konstantenwertmuster sind in Schlüsselpositionen erlaubt.

import constants

match config:
    case {"route": route}:
        process_route(route)
    case {constants.DEFAULT_PORT: sub_config, **rest}:
        process_config(sub_config, rest)

Das Subjekt muss eine Instanz von collections.abc.Mapping sein. Zusätzliche Schlüssel im Subjekt werden ignoriert, auch wenn **rest nicht vorhanden ist. Dies unterscheidet sich vom Sequenzmuster, bei dem zusätzliche Elemente zum Fehlschlagen eines Matches führen würden. Aber Mappings unterscheiden sich tatsächlich von Sequenzen: sie haben ein natürliches Verhalten bei struktureller Subtypisierung, d.h. die Übergabe eines Wörterbuchs mit zusätzlichen Schlüsseln irgendwo wird wahrscheinlich einfach funktionieren.

Aus diesem Grund ist **_ in Mapping-Mustern ungültig; es wäre immer eine Nulloperation, die ohne Konsequenz entfernt werden könnte.

Abgeglichene Schlüssel-Wert-Paare müssen bereits im Mapping vorhanden sein und dürfen nicht erst zur Laufzeit durch __missing__ oder __getitem__ erstellt werden. Zum Beispiel matchen collections.defaultdict-Instanzen nur Muster mit Schlüsseln, die bereits vorhanden waren, als der match-Block betreten wurde.

Klassenmuster

Vereinfachte Syntax

class_pattern:
    | name_or_attr '(' ')'
    | name_or_attr '(' ','.pattern+ ','? ')'
    | name_or_attr '(' ','.keyword_pattern+ ','? ')'
    | name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
keyword_pattern: NAME '=' or_pattern

Ein Klassenmuster bietet Unterstützung für das Dekonstruieren beliebiger Objekte. Es gibt zwei Möglichkeiten, Objektattribute abzugleichen: nach Position, wie bei Point(1, 2), und nach Namen, wie bei Point(x=1, y=2). Diese beiden können kombiniert werden, aber ein Positionsabgleich darf keinem Namensabgleich folgen. Jedes Element in einem Klassenmuster kann ein beliebiges Muster sein. Ein einfaches Beispiel

match shape:
    case Point(x, y):
        ...
    case Rectangle(x0, y0, x1, y1, painted=True):
        ...

Ob ein Abgleich erfolgreich ist oder nicht, wird durch das Äquivalent eines isinstance-Aufrufs bestimmt. Wenn das Subjekt (shape im Beispiel) keine Instanz der benannten Klasse (Point oder Rectangle) ist, schlägt der Abgleich fehl. Andernfalls wird er fortgesetzt (siehe Details im Abschnitt Laufzeit).

Die benannte Klasse muss von type erben. Sie kann ein einzelner Name oder ein punktierter Name sein (z.B. some_mod.SomeClass oder mod.pkg.Class). Der führende Name darf nicht _ sein, daher sind z.B. _(...) und _.C(...) ungültig. Verwenden Sie object(foo=_), um zu prüfen, ob das abgeglichene Objekt ein Attribut foo hat.

Standardmäßig können Unter-Muster nur per Schlüsselwort für benutzerdefinierte Klassen abgeglichen werden. Um Positions-Unter-Muster zu unterstützen, ist ein benutzerdefiniertes Attribut __match_args__ erforderlich. Die Laufzeitumgebung erlaubt den Abgleich mit beliebig verschachtelten Mustern, indem alle Instanzprüfungen und Attributsuchen entsprechend verkettet werden.

Kombination mehrerer Muster (ODER-Muster)

Mehrere alternative Muster können mit | zu einem zusammengefasst werden. Das bedeutet, das gesamte Muster stimmt überein, wenn mindestens eine Alternative übereinstimmt. Alternativen werden von links nach rechts ausprobiert und haben eine Kurzschluss-Eigenschaft; nachfolgende Muster werden nicht ausprobiert, wenn eines übereinstimmt. Beispiele

match something:
    case 0 | 1 | 2:
        print("Small number")
    case [] | [_]:
        print("A short sequence")
    case str() | bytes():
        print("Something string-like")
    case _:
        print("Something else")

Die Alternativen können Variablen binden, solange jede Alternative denselben Satz von Variablen bindet (außer _). Zum Beispiel

match something:
    case 1 | x:  # Error!
        ...
    case x | 1:  # Error!
        ...
    case one := [1] | two := [2]:  # Error!
        ...
    case Foo(arg=x) | Bar(arg=x):  # Valid, both arms bind 'x'
        ...
    case [x] | x:  # Valid, both arms bind 'x'
        ...

Guards

Jedes Top-Level-Muster kann von einem Guard der Form if expression gefolgt werden. Eine Case-Klausel ist erfolgreich, wenn das Muster übereinstimmt und der Guard zu einem wahren Wert ausgewertet wird. Zum Beispiel

match input:
    case [x, y] if x > MAX_INT and y > MAX_INT:
        print("Got a pair of large numbers")
    case x if x > MAX_INT:
        print("Got a large number")
    case [x, y] if x == y:
        print("Got equal items")
    case _:
        print("Not an outstanding input")

Wenn die Auswertung eines Guards eine Ausnahme auslöst, wird diese weitergegeben, anstatt die Case-Klausel fehlschlagen zu lassen. Namen, die in einem Muster vorkommen, werden gebunden, bevor der Guard erfolgreich ist. Dies wird also funktionieren

values = [0]

match values:
    case [x] if x:
        ...  # This is not executed
    case _:
        ...
print(x)  # This will print "0"

Beachten Sie, dass Guards für verschachtelte Muster nicht erlaubt sind, sodass [x if x > 0] ein SyntaxError ist und 1 | 2 if 3 | 4 als (1 | 2) if (3 | 4) geparst wird.

Walross-Muster

Es ist oft nützlich, ein Unter-Muster abzugleichen und den entsprechenden Wert an einen Namen zu binden. Zum Beispiel kann es nützlich sein, effizientere Abgleiche zu schreiben oder einfach Wiederholungen zu vermeiden. Um solche Fälle zu vereinfachen, kann jedem Muster (außer dem Walross-Muster selbst) ein Name und der Walross-Operator (:=) vorangestellt werden. Zum Beispiel

match get_shape():
    case Line(start := Point(x, y), end) if start == end:
        print(f"Zero length line at {x}, {y}")

Der Name links vom Walross-Operator kann in einem Guard, in der Match-Suite oder nach der Match-Anweisung verwendet werden. Der Name wird jedoch nur gebunden, wenn das Unter-Muster erfolgreich ist. Ein weiteres Beispiel

match group_shapes():
    case [], [point := Point(x, y), *other]:
        print(f"Got {point} in the second group")
        process_coordinates(x, y)
        ...

Technisch gesehen können die meisten dieser Beispiele mit Guards und/oder verschachtelten Match-Anweisungen neu geschrieben werden, dies wäre jedoch weniger lesbar und/oder würde weniger effizienten Code erzeugen. Grundsätzlich gelten die meisten Argumente in PEP 572 hier gleichermaßen.

Das Wildcard-Zeichen _ ist hier kein gültiger Name.

Laufzeitspezifikation

Das Match-Protokoll

Das Äquivalent eines isinstance-Aufrufs wird verwendet, um zu entscheiden, ob ein Objekt einem gegebenen Klassenmuster entspricht und um die entsprechenden Attribute zu extrahieren. Klassen, die unterschiedliche Abgleichsemantiken erfordern (wie Duck-Typing), können dies tun, indem sie __instancecheck__ (ein bereits vorhandener Metaklassen-Hook) definieren oder typing.Protocol verwenden.

Das Verfahren ist wie folgt

  • Das Klassenobjekt für Class in Class(<sub-patterns>) wird nachgeschlagen und isinstance(obj, Class) wird aufgerufen, wobei obj der Wert ist, der abgeglichen wird. Wenn falsch, schlägt der Abgleich fehl.
  • Wenn andererseits Unter-Muster in Form von Positions- oder Schlüsselwortargumenten angegeben sind, werden diese von links nach rechts wie folgt abgeglichen. Der Abgleich schlägt fehl, sobald ein Unter-Muster fehlschlägt; wenn alle Unter-Muster erfolgreich sind, ist der gesamte Klassenmusterabgleich erfolgreich.
  • Wenn es Übereinstimmungs-nach-Position-Elemente gibt und die Klasse ein Attribut __match_args__ hat, wird das Element an Position i gegen den Wert abgeglichen, der durch das Attribut __match_args__[i] nachgeschlagen wird. Zum Beispiel wird ein Muster Point2d(5, 8), wobei Point2d.__match_args__ == ["x", "y"], (ungefähr) übersetzt in obj.x == 5 and obj.y == 8.
  • Wenn es mehr Positions-Elemente als die Länge von __match_args__ gibt, wird ein TypeError ausgelöst.
  • Wenn das Attribut __match_args__ auf der abgeglichenen Klasse fehlt und ein oder mehrere Positions-Elemente in einem Abgleich vorkommen, wird ebenfalls ein TypeError ausgelöst. Wir greifen nicht auf __slots__ oder __annotations__ zurück – „Im Angesicht von Mehrdeutigkeit, widerstehe der Versuchung zu raten.“
  • Wenn es Übereinstimmungs-nach-Schlüsselwort-Elemente gibt, werden die Schlüsselwörter als Attribute des Subjekts nachgeschlagen. Wenn die Suche erfolgreich ist, wird der Wert gegen das entsprechende Unter-Muster abgeglichen. Wenn die Suche fehlschlägt, schlägt der Abgleich fehl.

Ein solches Protokoll bevorzugt Einfachheit der Implementierung gegenüber Flexibilität und Leistung. Für andere betrachtete Alternativen siehe erweitertes Matching.

Für die am häufigsten abgeglichenen integrierten Typen (bool, bytearray, bytes, dict, float, frozenset, int, list, set, str und tuple) ist es erlaubt, ein einzelnes Positions-Unter-Muster an den Aufruf zu übergeben. Anstatt gegen ein bestimmtes Attribut des Subjekts abgeglichen zu werden, wird es stattdessen gegen das Subjekt selbst abgeglichen. Dies erzeugt ein Verhalten, das für diese Objekte nützlich und intuitiv ist

  • bool(False) passt zu False (aber nicht zu 0).
  • tuple((0, 1, 2)) passt zu (0, 1, 2) (aber nicht zu [0, 1, 2]).
  • int(i) passt zu jedem int und bindet es an den Namen i.

Überlappende Teilmuster

Bestimmte Klassen von überlappenden Abgleichen werden zur Laufzeit erkannt und lösen Ausnahmen aus. Zusätzlich zu den grundlegenden Prüfungen, die im vorherigen Unterabschnitt beschrieben sind

  • Der Interpreter prüft, ob zwei Abgleichelemente nicht dasselbe Attribut anvisieren, z.B. ist Point2d(1, 2, y=3) ein Fehler.
  • Er prüft auch, ob ein Mapping-Muster versucht, denselben Schlüssel mehr als einmal abzugleichen.

Sonderattribut __match_args__

Das Attribut __match_args__ wird immer auf dem Typobjekt nachgeschlagen, das im Muster genannt wird. Wenn vorhanden, muss es eine Liste oder ein Tupel von Zeichenfolgen sein, die die erlaubten Positionsargumente benennen.

Bei der Entscheidung, welche Namen für den Abgleich verfügbar sein sollen, ist die empfohlene Vorgehensweise, dass Klassenmuster ein Spiegelbild der Konstruktion sein sollten; d.h. die Menge der verfügbaren Namen und ihre Typen sollten den Argumenten von __init__() ähneln.

Standardmäßig funktioniert nur der Abgleich nach Name. Klassen sollten __match_args__ als Klassenattribut definieren, wenn sie den Abgleich nach Position unterstützen möchten. Darüber hinaus unterstützen Dataclasses und benannte Tupel den Abgleich nach Position von Haus aus. Siehe unten für weitere Details.

Ausnahmen und Seiteneffekte

Beim Abgleichen jedes Falls kann die match-Anweisung die Ausführung anderer Funktionen auslösen (z.B. __getitem__(), __len__() oder eine Eigenschaft). Fast jede Ausnahme, die durch diese verursacht wird, breitet sich normal ausserhalb der match-Anweisung aus. Der einzige Fall, in dem eine Ausnahme nicht weitergegeben wird, ist ein AttributeError, der beim Versuch ausgelöst wird, ein Attribut beim Abgleich von Attributen eines Klassenmusters nachzuschlagen; dieser Fall führt nur zu einem Abgleichfehler, und der Rest der Anweisung wird normal fortgesetzt.

Der einzige Nebeneffekt, der explizit durch den Abgleichprozess übernommen wird, ist die Bindung von Namen. Der Prozess stützt sich jedoch auf Attributzugriff, Instanzprüfungen, len(), Gleichheit und Elementzugriff auf das Subjekt und einige seiner Komponenten. Er wertet auch konstante Wertemuster und die linke Seite von Klassenmustern aus. Während keine dieser typischerweise Nebeneffekte erzeugt, könnten einige dieser Objekte dies tun. Dieser Vorschlag lässt absichtlich jede Spezifikation aus, welche Methoden aufgerufen werden oder wie oft. Benutzercode, der sich auf dieses Verhalten stützt, sollte als fehlerhaft betrachtet werden.

Die Standardbibliothek

Um die Verwendung von Pattern Matching zu erleichtern, werden mehrere Änderungen an der Standardbibliothek vorgenommen

  • Namedtuples und Dataclasses erhalten automatisch generierte __match_args__.
  • Für Dataclasses ist die Reihenfolge der Attribute in den generierten __match_args__ dieselbe wie die Reihenfolge der entsprechenden Argumente in der generierten __init__() Methode. Dies schließt Situationen ein, in denen Attribute von einer Oberklasse geerbt werden.

Darüber hinaus wird ein systematischer Aufwand betrieben, um bestehende Klassen der Standardbibliothek durchzugehen und __match_args__ hinzuzufügen, wo es vorteilhaft erscheint.

Spezifikation für statische Prüfer

Prüfung auf Exhaustivität

Aus Zuverlässigkeitssicht zeigt die Erfahrung, dass das Verpassen eines Falls bei der Behandlung einer Menge möglicher Datenwerte zu schwer zu debuggenden Problemen führt, was die Leute zwingt, Sicherheits-Assertions wie diese hinzuzufügen

def get_first(data: Union[int, list[int]]) -> int:
    if isinstance(data, list) and data:
        return data[0]
    elif isinstance(data, int):
        return data
    else:
        assert False, "should never get here"

PEP 484 gibt an, dass statische Typenprüfer die Vollständigkeit von bedingten Prüfungen in Bezug auf Enum-Werte unterstützen sollten. PEP 586 verallgemeinerte diese Anforderung später auf Literal-Typen.

Diese PEP verallgemeinert diese Anforderung weiter auf beliebige Muster. Eine typische Situation, in der dies zutrifft, ist der Abgleich eines Ausdrucks mit einem Union-Typ

def classify(val: Union[int, Tuple[int, int], List[int]]) -> str:
    match val:
        case [x, y] if x > 0 and y > 0:
            return f"A pair of {x} and {y}"
        case [x, *other]:
            return f"A sequence starting with {x}"
        case int():
            return f"Some integer"
        # Type-checking error: some cases unhandled.

Die Vollständigkeitsprüfungen sollten auch dann gelten, wenn Pattern Matching und Enum-Werte kombiniert werden

from enum import Enum
from typing import Union

class Level(Enum):
    BASIC = 1
    ADVANCED = 2
    PRO = 3

class User:
    name: str
    level: Level

class Admin:
    name: str

account: Union[User, Admin]

match account:
    case Admin(name=name) | User(name=name, level=Level.PRO):
        ...
    case User(level=Level.ADVANCED):
        ...
    # Type-checking error: basic user unhandled

Offensichtlich ist kein Matchable-Protokoll (im Sinne von PEP 544) erforderlich, da jede Klasse abgleichbar ist und daher den oben genannten Prüfungen unterliegt.

Versiegelte Klassen als algebraische Datentypen

Oft ist es wünschenswert, die Vollständigkeit auf eine Menge von Klassen anzuwenden, ohne Ad-hoc-Union-Typen zu definieren, was selbst zerbrechlich ist, wenn eine Klasse in der Union-Definition fehlt. Ein Entwurfsmuster, bei dem eine Gruppe von rekordähnlichen Klassen zu einer Union kombiniert wird, ist in anderen Sprachen, die Pattern Matching unterstützen, beliebt und unter dem Namen algebraische Datentypen bekannt.

Wir schlagen vor, eine spezielle Dekorator-Klasse @sealed zum Modul typing hinzuzufügen, die zur Laufzeit keine Auswirkungen hat, aber statischen Typenprüfern anzeigt, dass alle Unterklassen (direkt und indirekt) dieser Klasse im selben Modul wie die Basisklasse definiert werden sollten.

Die Idee ist, dass, da alle Unterklassen bekannt sind, der Typenprüfer die versiegelte Basisklasse als Union aller ihrer Unterklassen behandeln kann. Zusammen mit Dataclasses ermöglicht dies eine saubere und sichere Unterstützung von algebraischen Datentypen in Python. Betrachten Sie dieses Beispiel

from dataclasses import dataclass
from typing import sealed

@sealed
class Node:
    ...

class Expression(Node):
    ...

class Statement(Node):
    ...

@dataclass
class Name(Expression):
    name: str

@dataclass
class Operation(Expression):
    left: Expression
    op: str
    right: Expression

@dataclass
class Assignment(Statement):
    target: str
    value: Expression

@dataclass
class Print(Statement):
    value: Expression

Mit einer solchen Definition kann ein Typenprüfer Node sicher als Union[Name, Operation, Assignment, Print] behandeln und auch z.B. Expression sicher als Union[Name, Operation] behandeln. Dies führt also zu einem Typenprüfungsfehler im folgenden Snippet, da Name nicht behandelt wird (und der Typenprüfer eine nützliche Fehlermeldung geben kann)

def dump(node: Node) -> str:
    match node:
        case Assignment(target, value):
            return f"{target} = {dump(value)}"
        case Print(value):
            return f"print({dump(value)})"
        case Operation(left, op, right):
            return f"({dump(left)} {op} {dump(right)})"

Typen-Erasure

Klassenmuster unterliegen der Laufzeit-Typenerasur. Das heißt, obwohl man einen Typalias IntQueue = Queue[int] definieren kann, so dass ein Muster wie IntQueue() syntaktisch gültig ist, sollten Typenprüfer einen solchen Abgleich ablehnen

queue: Union[Queue[int], Queue[str]]
match queue:
    case IntQueue():  # Type-checking error here
        ...

Beachten Sie, dass das obige Snippet tatsächlich zur Laufzeit mit der aktuellen Implementierung von generischen Klassen im Modul typing sowie mit integrierten generischen Klassen in der kürzlich angenommenen PEP 585 fehlschlägt, da sie isinstance-Prüfungen verbieten.

Um dies zu verdeutlichen: Generische Klassen sind im Allgemeinen nicht vom Pattern Matching ausgeschlossen, nur ihre Typparameter können nicht explizit angegeben werden. Es ist immer noch in Ordnung, wenn Unter-Muster oder Literale Typvariablen binden. Zum Beispiel

from typing import Generic, TypeVar, Union

T = TypeVar('T')

class Result(Generic[T]):
    first: T
    other: list[T]

result: Union[Result[int], Result[str]]

match result:
    case Result(first=int()):
        ...  # Type of result is Result[int] here
    case Result(other=["foo", "bar", *rest]):
        ...  # Type of result is Result[str] here

Hinweis zu Konstanten

Die Tatsache, dass ein Capture-Muster immer ein Zuweisungsziel ist, kann unerwünschte Konsequenzen haben, wenn ein Benutzer versehentlich versucht, einen Wert mit einer Konstante abzugleichen, anstatt das konstante Wertemuster zu verwenden. Infolgedessen wird ein solcher Abgleich zur Laufzeit immer erfolgreich sein und zudem den Wert der Konstante überschreiben. Es ist daher wichtig, dass statische Typenprüfer vor solchen Situationen warnen. Zum Beispiel

from typing import Final

MAX_INT: Final = 2 ** 64

value = 0

match value:
    case MAX_INT:  # Type-checking error here: cannot assign to final name
        print("Got big number")
    case _:
        print("Something else")

Beachten Sie, dass die CPython-Referenzimplementierung auch eine SyntaxWarning-Nachricht für diesen Fall generiert.

Präzise Typenprüfung von Stern-Mustern

Typenprüfer sollten präzise Typenprüfungen für Sternchen-Elemente im Pattern Matching durchführen und ihnen entweder einen heterogenen list[T]-Typ oder einen TypedDict-Typ geben, wie in PEP 589 spezifiziert. Zum Beispiel

stuff: Tuple[int, str, str, float]

match stuff:
    case a, *b, 0.5:
        # Here a is int and b is list[str]
        ...

Leistungsbetrachtungen

Idealerweise sollte eine match-Anweisung im Vergleich zu einer äquivalenten Kette von if-Anweisungen eine gute Laufzeitleistung aufweisen. Obwohl die Geschichte der Programmiersprachen reich an Beispielen für neue Funktionen ist, die die Produktivität der Ingenieure auf Kosten zusätzlicher CPU-Zyklen erhöht haben, wäre es bedauerlich, wenn die Vorteile von match durch eine signifikante Gesamtverringerung der Laufzeitleistung ausgeglichen würden.

Obwohl diese PEP keine bestimmte Implementierungsstrategie vorgibt, sind ein paar Worte zur Prototypimplementierung und wie sie versucht, die Leistung zu maximieren, angebracht.

Grundsätzlich wandelt die Prototypimplementierung die gesamte Syntax der match-Anweisung in äquivalente if/else-Blöcke um – oder genauer gesagt, in Python-Bytecodes, die die gleiche Wirkung haben. Mit anderen Worten, die gesamte Logik zum Testen von Instanztypen, Sequenzlängen, Mapping-Schlüsseln usw. ist anstelle des match inlineiert.

Dies ist nicht die einzige mögliche Strategie und auch nicht unbedingt die beste. Beispielsweise könnten die Instanzprüfungen memoisiert werden, insbesondere wenn in einer einzigen match-Anweisung mehrere Instanzen desselben Klassentyps mit unterschiedlichen Argumenten vorhanden sind. Es ist auch theoretisch möglich, dass eine zukünftige Implementierung Fallklauseln oder Unter-Muster parallel mithilfe eines Entscheidungsbaums verarbeitet, anstatt sie einzeln zu testen.

Abwärtskompatibilität

Diese PEP ist vollständig abwärtskompatibel: die Schlüsselwörter match und case werden vorgeschlagen, um (und bleiben!) Soft-Keywords zu sein, sodass ihre Verwendung als Variablen-, Funktions-, Klassen-, Modul- oder Attributnamen überhaupt nicht beeinträchtigt wird.

Dies ist wichtig, da match der Name einer beliebten und bekannten Funktion und Methode im Modul re ist, die wir weder brechen noch als veraltet markieren wollen.

Der Unterschied zwischen Hard- und Soft-Keywords besteht darin, dass Hard-Keywords immer reservierte Wörter sind, selbst an Stellen, wo sie keinen Sinn ergeben (z.B. x = class + 1), während Soft-Keywords nur im Kontext eine besondere Bedeutung haben. Da der Parser seit PEP 617 zurückverfolgt, bedeutet dies, dass er bei verschiedenen Versuchen, einen Codefragment zu parsen, ein Soft-Keyword anders interpretieren könnte.

Nehmen wir an, der Parser stößt auf die folgende Eingabe

match [x, y]:

Der Parser versucht zunächst, dies als Ausdrucksanweisung zu parsen. Er interpretiert match als NAME-Token und betrachtet dann [x, y] als doppelte Indizierung. Dann stößt er auf den Doppelpunkt und muss zurückverfolgen, da eine Ausdrucksanweisung nicht von einem Doppelpunkt gefolgt werden kann. Der Parser verfolgt dann zum Anfang der Zeile zurück und stellt fest, dass match ein Soft-Keyword ist, das an dieser Stelle erlaubt ist. Dann betrachtet er [x, y] als Listenexpression. Der Doppelpunkt ist dann genau das, was der Parser erwartet hat, und das Parsen gelingt.

Auswirkungen auf Drittanbieter-Tools

Es gibt viele Werkzeuge im Python-Ökosystem, die mit Python-Quellcode arbeiten: Linters, Syntax-Highlighter, Auto-Formatter und IDEs. Diese müssen alle aktualisiert werden, um die match-Anweisung zu berücksichtigen.

Im Allgemeinen fallen diese Werkzeuge in eine von zwei Kategorien

Flache Parser versuchen nicht, die vollständige Syntax von Python zu verstehen, sondern scannen den Quellcode nach spezifischen bekannten Mustern. IDEs wie Visual Studio Code, Emacs und TextMate fallen tendenziell in diese Kategorie, da der Quellcode während der Bearbeitung häufig ungültig ist und ein strenger Ansatz zum Parsen fehlschlagen würde.

Für diese Art von Werkzeugen ist das Hinzufügen von Kenntnissen über ein neues Schlüsselwort relativ einfach, nur eine Ergänzung einer Tabelle oder vielleicht eine Modifikation eines regulären Ausdrucks.

Tiefe Parser verstehen die vollständige Syntax von Python. Ein Beispiel dafür ist der Auto-Formatter Black. Eine besondere Anforderung für diese Art von Werkzeugen ist, dass sie nicht nur die Syntax der aktuellen Python-Version, sondern auch ältere Versionen verstehen müssen.

Die match-Anweisung verwendet ein Soft-Keyword und ist eine der ersten wichtigen Python-Funktionen, die die Fähigkeiten des neuen PEG-Parsers nutzen. Das bedeutet, dass Nicht-PEG-kompatible Parser von Drittanbietern Schwierigkeiten mit der neuen Syntax haben werden.

Es wurde festgestellt, dass eine Reihe dieser Tools von Drittanbietern gängige Parsing-Bibliotheken verwenden (Black verwendet beispielsweise einen Fork des lib2to3-Parsers). Es kann hilfreich sein, weit verbreitete Parsing-Bibliotheken (wie parso und libCST) zu identifizieren und sie PEG-kompatibel zu aktualisieren.

Da diese Arbeit jedoch nicht nur für die match-Anweisung, sondern für jede neue Python-Syntax, die die Fähigkeiten des PEG-Parsers nutzt, durchgeführt werden müsste, wird dies als außerhalb des Geltungsbereichs dieser PEP betrachtet. (Obwohl vorgeschlagen wird, dass dies ein gutes Summer of Code-Projekt wäre.)

Referenzimplementierung

Eine feature-complete CPython-Implementierung ist auf GitHub verfügbar.

Ein interaktiver Playground, der auf der obigen Implementierung basiert, wurde mit Binder und Jupyter erstellt.

Beispielcode

Eine kleine Sammlung von Beispielcode ist auf GitHub verfügbar.

Abgelehnte Ideen

Diese allgemeine Idee schwebt schon ziemlich lange herum, und viele Entscheidungen wurden hin und her getroffen. Hier fassen wir viele alternative Wege zusammen, die eingeschlagen, aber schließlich aufgegeben wurden.

Tun Sie dies nicht, Pattern Matching ist schwer zu lernen

Unserer Meinung nach ist das vorgeschlagene Pattern Matching nicht schwieriger als das Hinzufügen von isinstance() und getattr() zur iterierbaren Entpackung. Außerdem glauben wir, dass die vorgeschlagene Syntax die Lesbarkeit für eine Vielzahl von Code-Mustern erheblich verbessert, indem sie erlaubt, was man tun möchte, anstatt wie man es tun möchte. Wir hoffen, dass die wenigen echten Code-Schnipsel, die wir oben in der PEP aufgenommen haben, diesen Vergleich gut genug veranschaulichen. Für weitere echte Codebeispiele und ihre Übersetzungen siehe Ref. [1].

Tun Sie dies nicht, verwenden Sie bestehende Mechanismen zur Methodenverteilung

Wir erkennen an, dass einige der Anwendungsfälle für die match-Anweisung mit dem, was mit traditionellen objektorientierten (OO) Entwurfstechniken unter Verwendung von Klassenvererbung erreicht werden kann, überlappen. Die Fähigkeit, alternative Verhaltensweisen basierend auf dem Testen des Laufzeittyps eines Match-Subjekts auszuwählen, mag strengen OO-Puristen sogar ketzerisch erscheinen.

Python war jedoch schon immer eine Sprache, die eine Vielzahl von Programmierstilen und Paradigmen umarmt. Klassische Python-Idiome wie "Duck"-Typing gehen über das traditionelle OO-Modell hinaus.

Wir glauben, dass es wichtige Anwendungsfälle gibt, bei denen die Verwendung von match zu einer saubereren und wartbareren Architektur führt. Diese Anwendungsfälle zeichnen sich tendenziell durch eine Reihe von Merkmalen aus

  • Algorithmen, die traditionelle Linien der Datenkapselung durchschneiden. Wenn ein Algorithmus heterogene Elemente unterschiedlicher Typen verarbeitet (wie z.B. die Auswertung oder Transformation eines abstrakten Syntaxbaums oder algebraische Manipulation mathematischer Symbole), zwingt dies den Benutzer, den Algorithmus als einzelne Methoden für jeden Elementtyp zu implementieren, was zu Logik führt, die über die gesamte Codebasis verstreut ist, anstatt sauber an einem Ort lokalisiert zu sein.
  • Programmarchitekturen, bei denen die Menge der möglichen Datentypen relativ stabil ist, aber eine ständig wachsende Menge von Operationen auf diesen Datentypen ausgeführt werden muss. Dies in einem strengen OO-Ansatz zu tun, erfordert das ständige Hinzufügen neuer Methoden sowohl zur Basisklasse als auch zu den Unterklassen, um die neuen Methoden zu unterstützen, was die Basisklasse mit vielen sehr spezialisierten Methodendefinitionen "verschmutzt" und weit verbreitete Störungen und Änderungen im Code verursacht. Im Gegensatz dazu beinhaltet bei einer match-basierten Verteilung das Hinzufügen eines neuen Verhaltens lediglich das Schreiben einer neuen match-Anweisung.
  • OO behandelt auch keine Verteilung basierend auf der Form eines Objekts, wie z.B. der Länge eines Tupels oder der Anwesenheit eines Attributs – stattdessen muss jede solche Verteilungsentscheidung in den Typ des Objekts kodiert werden. Formbasierte Verteilung ist besonders interessant, wenn es um die Behandlung von "Duck"-typed Objekten geht.

Wo OO eindeutig überlegen ist, ist im umgekehrten Fall: Wenn die Menge der möglichen Operationen relativ stabil und gut definiert ist, aber eine ständig wachsende Menge von Datentypen zu operieren ist. Ein klassisches Beispiel dafür sind UI-Widget-Toolkits, bei denen es eine feste Menge von Interaktionstypen gibt (Neuzeichnen, Mausklick, Tastendruck usw.), aber die Menge der Widget-Typen ständig wächst, wenn Entwickler neue und kreative Benutzerinteraktionsstile erfinden. Das Hinzufügen einer neuen Art von Widget ist eine einfache Angelegenheit des Schreibens einer neuen Unterklasse, während Sie mit einem Match-basierten Ansatz eine neue Case-Klausel zu vielen weit verbreiteten Match-Anweisungen hinzufügen müssen. Wir empfehlen daher nicht, match in einer solchen Situation zu verwenden.

Erlauben Sie stattdessen flexiblere Zuweisungsziele

Es gab die Idee, stattdessen einfach die iterable Entpackung zu allgemeineren Zuweisungszielen zu verallgemeinern, anstatt eine neue Art von Anweisung hinzuzufügen. Dieses Konzept ist in einigen anderen Sprachen als "unwiderrufliche Abgleiche" bekannt. Wir haben uns dagegen entschieden, weil die Untersuchung realer potenzieller Anwendungsfälle zeigte, dass in den meisten Fällen die Dekonstruktion mit einer if-Bedingung zusammenhängt. Außerdem sind viele davon in einer Reihe von exklusiven Entscheidungen gruppiert.

Machen Sie es zu einem Ausdruck

In den meisten anderen Sprachen wird Pattern Matching als Ausdruck dargestellt, nicht als Anweisung. Aber es zu einem Ausdruck zu machen, wäre inkonsistent mit anderen syntaktischen Entscheidungen in Python. Alle Entscheidungsfindungslogik wird fast ausschließlich in Anweisungen ausgedrückt, daher haben wir uns entschieden, davon nicht abzuweichen.

Verwenden Sie ein festes Schlüsselwort

Es gab Optionen, match zu einem Hard-Keyword zu machen oder ein anderes Keyword zu wählen. Obwohl die Verwendung eines Hard-Keywords das Leben für simpel gestrickte Syntax-Highlighter vereinfachen würde, haben wir uns aus mehreren Gründen dagegen entschieden

  • Am wichtigsten ist, dass der neue Parser dies nicht von uns verlangt. Im Gegensatz zu async, das über mehrere Versionen hinweg Schwierigkeiten als Soft-Keyword verursachte, können wir match hier zu einem permanenten Soft-Keyword machen.
  • match wird im vorhandenen Code so häufig verwendet, dass es fast jedes bestehende Programm brechen würde und eine Belastung für die Fehlerbehebung auf viele Leute legen würde, die möglicherweise nicht einmal von der neuen Syntax profitieren.
  • Es ist schwierig, ein alternatives Schlüsselwort zu finden, das nicht üblicherweise im vorhandenen Code als Bezeichner verwendet wird und trotzdem die Bedeutung der Anweisung klar widerspiegelt.

Verwenden Sie as oder | anstelle von case für Case-Klauseln

Das hier vorgeschlagene Pattern Matching ist eine Kombination aus Mehrzweig-Kontrollfluss (im Einklang mit switch in Algol-abgeleiteten Sprachen oder cond in Lisp) und Objekt-Dekonstruktion, wie sie in funktionalen Sprachen vorkommt. Während das vorgeschlagene Schlüsselwort case den Mehrzweig-Aspekt hervorhebt, wären alternative Schlüsselwörter wie as gleichermaßen möglich und würden den Dekonstruktions-Aspekt hervorheben. as oder with haben beispielsweise auch den Vorteil, dass sie bereits Schlüsselwörter in Python sind. Da case als Schlüsselwort jedoch nur als führendes Schlüsselwort innerhalb einer match-Anweisung vorkommen kann, ist es für einen Parser einfach, zwischen seiner Verwendung als Schlüsselwort oder als Variable zu unterscheiden.

Andere Varianten würden ein Symbol wie | oder => verwenden oder ganz ohne spezielles Kennzeichen auskommen.

Da Python eine statement-orientierte Sprache in der Tradition von Algol ist und da jede zusammengesetzte Anweisung mit einem identifizierenden Schlüsselwort beginnt, schien case am besten mit Pythons Stil und Traditionen übereinzustimmen.

Verwenden Sie ein flaches Einrückungsschema

Es gab die Idee, ein alternatives Einrückungsschema zu verwenden, z.B. wo jede Case-Klausel nicht relativ zum ursprünglichen match-Teil eingerückt wäre

match expression:
case pattern_1:
    ...
case pattern_2:
    ...

Die Motivation ist, dass flache Einrückungen zwar horizontalen Platz sparen, aber für das Auge eines Python-Programmierers seltsam aussehen können, da überall sonst auf einen Doppelpunkt eine Einrückung folgt. Dies wird auch das Leben für simpel gestrickte Code-Editoren erschweren. Schließlich kann das Problem des horizontalen Platzes durch die Zulassung einer "Halb-Einrückung" (d.h. zwei Leerzeichen anstelle von vier) für Match-Anweisungen gelöst werden.

In Beispielprogrammen, die match verwenden und im Rahmen der Entwicklung dieser PEP geschrieben wurden, wird eine spürbare Verbesserung der Kürze des Codes beobachtet, die den zusätzlichen Einrückungsgrad mehr als wettmacht.

Ein weiterer erwogener Vorschlag war die Verwendung einer flachen Einrückung, die Expression jedoch in die Zeile nach match: setzt, wie folgt:

match:
    expression
case pattern_1:
    ...
case pattern_2:
    ...

Dies wurde letztendlich abgelehnt, da der erste Block eine Neuerung in der Python-Grammatik darstellen würde: ein Block, dessen einziger Inhalt eine einzelne Expression und keine Folge von Anweisungen ist.

Alternativen für Muster für konstante Werte

Dies ist wahrscheinlich der kniffligste Punkt. Das Abgleichen mit vordefinierten Konstanten ist sehr üblich, aber die dynamische Natur von Python macht es auch mehrdeutig im Vergleich zu Capture-Mustern. Fünf weitere Alternativen wurden in Betracht gezogen

  • Verwendung einiger impliziter Regeln. Wenn beispielsweise ein Name im globalen Geltungsbereich definiert war, verwies er auf eine Konstante und stellte kein Capture-Muster dar
    # Here, the name "spam" must be defined in the global scope (and
    # not shadowed locally). "side" must be local.
    
    match entree[-1]:
        case spam: ...  # Compares entree[-1] == spam.
        case side: ...  # Assigns side = entree[-1].
    

    Dies kann jedoch zu Überraschungen und Fernwirkungen führen, wenn jemand vor der Match-Anweisung einen nicht verwandten, übereinstimmenden Namen definiert.

  • Verwendung einer Regel, die auf der Groß-/Kleinschreibung eines Namens basiert. Insbesondere, wenn der Name mit einem Kleinbuchstaben beginnt, wäre es ein Capture-Muster, während er, wenn er mit einem Großbuchstaben beginnt, auf eine Konstante verweisen würde
    match entree[-1]:
        case SPAM: ...  # Compares entree[-1] == SPAM.
        case side: ...  # Assigns side = entree[-1].
    

    Dies funktioniert gut mit den Empfehlungen zur Benennung von Konstanten aus PEP 8. Der Haupteinwand ist, dass es keinen anderen Teil des Kern-Pythons gibt, in dem die Groß-/Kleinschreibung eines Namens semantisch bedeutsam ist. Darüber hinaus erlaubt Python Bezeichnern die Verwendung verschiedener Schriftsysteme, von denen viele (z. B. CJK) keine Groß-/Kleinschreibung unterscheiden.

  • Verwendung zusätzlicher Klammern, um die Nachschlagesemantik für einen gegebenen Namen anzuzeigen. Zum Beispiel
    match entree[-1]:
        case (spam): ...  # Compares entree[-1] == spam.
        case side: ...    # Assigns side = entree[-1].
    

    Dies mag eine praktikable Option sein, kann aber bei häufiger Verwendung zu visuellen Störungen führen. Außerdem sieht es ehrlich gesagt ziemlich ungewöhnlich aus, besonders in verschachtelten Kontexten.

    Dies hat auch das Problem, dass wir möglicherweise Klammern benötigen, um Gruppierungen in Mustern zu disambiguieren, z. B. in Point(x, y=(y := complex())).

  • Einführung eines speziellen Symbols, z. B. ., ?, $ oder ^, um anzuzeigen, dass ein gegebener Name ein Wert ist, gegen den abgeglichen werden soll, und nicht zugewiesen werden soll. Eine frühere Version dieses Vorschlags verwendete eine Regel mit einem führenden Punkt
    match entree[-1]:
        case .spam: ...  # Compares entree[-1] == spam.
        case side: ...   # Assigns side = entree[-1].
    

    Obwohl potenziell nützlich, führt es zu seltsam aussehender neuer Syntax, ohne die Muster-Syntax ausdrucksstärker zu machen. Tatsächlich können benannte Konstanten mit den bestehenden Regeln funktionsfähig gemacht werden, indem sie in Enum-Typen konvertiert oder in ihren eigenen Namensraum eingeschlossen werden (von den Autoren als eine honking great idea betrachtet)

    match entree[-1]:
        case Sides.SPAM: ...  # Compares entree[-1] == Sides.SPAM.
        case side: ...        # Assigns side = entree[-1].
    

    Bei Bedarf könnte die Regel mit führendem Punkt (oder eine ähnliche Variante) später ohne Rückwärtskompatibilitätsprobleme wieder hinzugefügt werden.

  • Es gab auch die Idee, die Nachschlagesemantik zum Standard zu machen und $ oder ? in Capture-Mustern zu verwenden
    match entree[-1]:
        case spam: ...   # Compares entree[-1] == spam.
        case side?: ...  # Assigns side = entree[-1].
    

    Dabei gibt es einige Probleme

    • Capture-Muster sind in typischem Code üblicher, daher ist es unerwünscht, eine spezielle Syntax dafür zu verlangen.
    • Die Autoren sind keine andere Sprache bekannt, die Captures auf diese Weise verziert.
    • Keine der vorgeschlagenen Syntaxe hat einen Präzedenzfall in Python; kein anderer Ort in Python, der Namen bindet (z. B. import, def, for), verwendet spezielle Markierungssyntax.
    • Es würde die syntaktischen Parallelen der aktuellen Grammatik brechen
      match coords:
          case ($x, $y):
              return Point(x, y)  # Why not "Point($x, $y)"?
      

Letztendlich wurden diese Alternativen aufgrund der genannten Nachteile abgelehnt.

Verbieten Sie Gleitkomma-Literale in Mustern

Aufgrund der Ungenauigkeit von Gleitkommazahlen erlaubte eine frühe Version dieses Vorschlags nicht die Verwendung von Gleitkommakonstanten als Muster für Übereinstimmungen. Ein Teil der Begründung für dieses Verbot ist, dass Rust dies tut.

Während der Implementierung wurde jedoch festgestellt, dass die Unterscheidung zwischen Gleitkommawerten und anderen Typen zusätzlichen Code in der VM erforderte, der Übereinstimmungen im Allgemeinen verlangsamen würde. Da Python und Rust sehr unterschiedliche Sprachen mit unterschiedlichen Benutzergemeinschaften und zugrunde liegenden Philosophien sind, wurde davon ausgegangen, dass die Zulassung von Gleitkommaliteralen nicht zu viel Schaden anrichten und für die Benutzer weniger überraschend sein würde.

Bereichs-Matching-Muster

Dies würde Muster wie 1...6 zulassen. Es gibt jedoch eine Reihe von Mehrdeutigkeiten

  • Ist der Bereich offen, halboffen oder geschlossen? (D. h. ist 6 im obigen Beispiel enthalten oder nicht?)
  • Passt der Bereich zu einer einzelnen Zahl oder einem Bereichsobjekt?
  • Bereichsabgleiche werden oft für Zeichenbereiche ('a'...'z') verwendet, aber das funktioniert in Python nicht, da es keinen Zeichendatentyp gibt, nur Strings.
  • Bereichsabgleiche können eine bedeutende Leistungsoptimierung sein, wenn Sie eine Sprungtabelle vorab erstellen können, aber das ist in Python aufgrund der Tatsache, dass Namen dynamisch neu gebunden werden können, im Allgemeinen nicht möglich.

Anstatt eine spezielle Syntax für Bereiche zu erstellen, wurde beschlossen, dass die Zulassung benutzerdefinierter Musterobjekte (InRange(0, 6)) flexibler und weniger mehrdeutig wäre; diese Ideen wurden jedoch vorerst verschoben (siehe verschobene Ideen).

Verwenden Sie Dispatch-Dict-Semantik für Matches

Implementierungen für klassische switch-Anweisungen verwenden manchmal eine vortrainierte Hash-Tabelle anstelle von verketteten Gleichheitsvergleichen, um eine gewisse Leistung zu erzielen. Im Kontext der match-Anweisung ist dies technisch auch für Übereinstimmungen mit literalen Mustern möglich. Unterschiedliche Semantiken für verschiedene Arten von Mustern wären jedoch für einen potenziell geringen Leistungsgewinn zu überraschend.

Wir können immer noch mögliche Leistungsoptimierungen in dieser Richtung experimentieren, wenn sie keine semantischen Unterschiede verursachen.

Verwenden Sie continue und break in Case-Klauseln.

Ein weiterer abgelehnter Vorschlag war, continue und break innerhalb von match neue Bedeutungen zu geben, was folgendes Verhalten hätte

  • continue würde die aktuelle Case-Klausel verlassen und die Übereinstimmung mit der nächsten Case-Klausel fortsetzen.
  • break würde die Match-Anweisung verlassen.

Es gibt jedoch einen ernsthaften Nachteil dieses Vorschlags: Wenn die match-Anweisung in eine Schleife verschachtelt ist, ändern sich die Bedeutungen von continue und break. Dies kann zu unerwartetem Verhalten bei Refactorings führen; außerdem kann argumentiert werden, dass es andere Mittel gibt, um das gleiche Verhalten zu erzielen (z. B. die Verwendung von Guard-Bedingungen), und dass das bestehende Verhalten von continue und break in der Praxis weitaus nützlicher ist.

UND (&) Muster

Dieser Vorschlag definiert ein OR-Muster (|), um eine von mehreren Alternativen abzugleichen; warum nicht auch ein AND-Muster (&)? Besonders angesichts der Tatsache, dass andere Sprachen (z. B. F#) dies unterstützen.

Es ist jedoch nicht klar, wie nützlich dies wäre. Die Semantik für das Abgleichen von Dictionaries, Objekten und Sequenzen beinhaltet bereits ein implizites „und“: Alle genannten Attribute und Elemente müssen vorhanden sein, damit der Abgleich erfolgreich ist. Guard-Bedingungen können auch viele der Anwendungsfälle unterstützen, für die ein hypothetischer „und“-Operator verwendet würde.

Letztendlich wurde beschlossen, dass dies die Syntax komplexer macht, ohne einen signifikanten Vorteil zu bringen.

Negative Match-Muster

Eine Negation eines Musterabgleichs mit dem Operator ! als Präfix würde genau dann übereinstimmen, wenn das Muster selbst nicht übereinstimmt. Zum Beispiel würde !(3 | 4) alles außer 3 oder 4 abgleichen.

Dies wurde abgelehnt, da dokumentierte Beweise dafür vorliegen, dass diese Funktion (in Sprachen, die sie unterstützen) selten nützlich ist oder als doppelte Negation !! zur Steuerung von Variablenbereichen und zur Verhinderung von Variablenbindungen verwendet wird (was für Python nicht zutrifft). Sie kann auch mit Guard-Bedingungen simuliert werden.

Prüfen Sie Exhaustivität zur Laufzeit

Die Frage ist, was zu tun ist, wenn keine Case-Klausel ein übereinstimmendes Muster hat und keine Standardklausel vorhanden ist. Eine frühere Version des Vorschlags sah vor, dass das Verhalten in diesem Fall darin bestünde, eine Ausnahme auszulösen, anstatt leise durchzufallen.

Die Argumente hin und her waren viele, aber am Ende gewann das EIBTI (Explicit Is Better Than Implicit) Argument: Es ist besser, den Programmierer explizit eine Ausnahme auslösen zu lassen, wenn dies das gewünschte Verhalten ist.

Für Fälle wie versiegelte Klassen und Aufzählungen, bei denen die Muster alle Mitglieder einer diskreten Menge sein sollen, können statische Prüfer vor fehlenden Mustern warnen.

Typ-Annotationen für Muster-Variablen

Der Vorschlag war, Muster mit Typannotationen zu kombinieren

match x:
    case [a: int, b: str]: print(f"An int {a} and a string {b}:)
    case [a: int, b: int, c: int]: print(f"Three ints", a, b, c)
    ...

Diese Idee hat viele Probleme. Zum einen kann der Doppelpunkt nur innerhalb von Klammern oder runden Klammern verwendet werden, sonst wird die Syntax mehrdeutig. Und da Python isinstance()-Prüfungen für generische Typen nicht zulässt, funktionieren Typannotationen, die Generics enthalten, nicht wie erwartet.

Erlauben Sie *rest in Klassenmustern

Es wurde vorgeschlagen, *rest in einem Klassenmuster zuzulassen, wobei eine Variable an alle Positionsargumente gebunden wird (ähnlich wie bei Entpackungszuweisungen). Dies würde eine gewisse Symmetrie mit Sequenzmustern bieten. Es könnte jedoch mit einer Funktion verwechselt werden, die die *Werte* für alle Positionsargumente auf einmal bereitstellt. Und es scheint keinen praktischen Bedarf dafür zu geben, daher wurde es verworfen. (Es könnte leicht zu einem späteren Zeitpunkt hinzugefügt werden, wenn ein Bedarf entsteht.)

Verbieten Sie _.a in Mustern für konstante Werte

Der erste öffentliche Entwurf besagte, dass der anfängliche Name in einem Muster für konstante Werte nicht _ sein darf, da _ eine spezielle Bedeutung bei Musterabgleichen hat, daher wäre dies ungültig

case _.a: ...

(Allerdings wäre a._ legal und würde das Attribut mit dem Namen _ des Objekts a wie üblich laden.)

Es gab einige Gegenreaktionen auf diese Einschränkung auf python-dev (einige Leute haben einen legitimen Verwendungszweck für _ als wichtige globale Variable, insbesondere in i18n), und der einzige Grund für dieses Verbot war, Verwirrung bei den Benutzern zu vermeiden. Aber es ist nicht der Hügel, auf dem man sterben muss.

Verwenden Sie ein anderes Token als Platzhalter

Es wurde vorgeschlagen, ... (d. h. das Auslassungszeichen) oder * (Sternchen) als Platzhalter zu verwenden. Beide sehen jedoch so aus, als ob eine beliebige Anzahl von Elementen weggelassen wird

case [a, ..., z]: ...
case [a, *, z]: ...

Beide sehen so aus, als würden sie eine Sequenz von mindestens zwei Elementen abgleichen und die ersten und letzten Werte erfassen.

Zusätzlich müssten wir, wenn * als Platzhalterzeichen verwendet würde, eine andere Möglichkeit finden, den Rest einer Sequenz zu erfassen, die derzeit so geschrieben wird

case [first, second, *rest]: ...

Die Verwendung einer Auslassung wäre auch in Dokumentation und Beispielen verwirrender, wo ... routinemäßig verwendet wird, um etwas Offensichtliches oder Irrelevantes anzuzeigen. (Ja, dies wäre auch ein Argument gegen die anderen Verwendungen von ... in Python, aber das Wasser ist bereits unter der Brücke.)

Ein weiterer Vorschlag war die Verwendung von ?. Dies könnte akzeptabel sein, obwohl es den Tokenizer ändern würde.

Außerdem wird _ bereits als Wegwerfziel in anderen Kontexten verwendet, und diese Verwendung ist ziemlich ähnlich. Dieses Beispiel stammt aus difflib.py in der Standardbibliothek

for tag, _, _, j1, j2 in group: ...

Das vielleicht überzeugendste Argument ist, dass _ in jeder anderen Sprache, die wir uns angesehen haben, die Musterabgleich unterstützt, als Platzhalter verwendet wird: C#, Elixir, Erlang, F#, Haskell, Mathematica, OCaml, Ruby, Rust, Scala und Swift. Nun, im Allgemeinen sollten wir uns nicht zu sehr darum kümmern, was eine andere Sprache tut, da Python eindeutig anders ist als all diese Sprachen. Wenn es jedoch einen so überwältigenden und starken Konsens gibt, sollte Python sich nicht bemühen, etwas völlig anderes zu tun – insbesondere angesichts der Tatsache, dass _ in Python gut funktioniert und bereits als Wegwerfziel verwendet wird.

Beachten Sie, dass _ nicht von Mustern zugewiesen wird – dies vermeidet Konflikte mit der Verwendung von _ als Markierung für übersetzbare Zeichenfolgen und einem Alias für gettext.gettext, wie von der Dokumentation des gettext-Moduls empfohlen.

Verwenden Sie eine andere Syntax anstelle von | für ODER-Muster

Es wurden einige Alternativen zur Verwendung von | zur Trennung der Alternativen in OR-Mustern vorgeschlagen. Anstelle von

case 401|403|404:
    print("Some HTTP error")

wurden die folgenden Vorschläge unterbreitet

  • Verwendung eines Kommas
    case 401, 403, 404:
      print("Some HTTP error")
    

    Dies sieht einem Tupel zu ähnlich aus – wir müssten eine andere Art der Schreibung von Tupeln finden, und der Konstrukt müsste innerhalb der Argumentliste eines Klassenmusters in Klammern gesetzt werden. Im Allgemeinen haben Kommas bereits viele verschiedene Bedeutungen in Python, wir sollten nicht noch mehr hinzufügen.

  • Gestapelte Fälle zulassen
    case 401:
    case 403:
    case 404:
      print("Some HTTP error")
    

    So würde dies in C geschehen, unter Verwendung seiner Fall-Through-Semantik für Fälle. Wir wollen jedoch nicht den Eindruck erwecken, dass match/case Fall-Through-Semantik verwendet (die eine häufige Fehlerquelle in C darstellt). Außerdem wäre dies ein neuartiges Einrückungsmuster, das möglicherweise die Unterstützung in IDEs usw. erschwert (es würde die einfache Regel brechen: „Füge eine Einrückungsebene nach einer Zeile hinzu, die mit einem Doppelpunkt endet“). Schließlich würde dies verschachtelte OR-Muster nicht unterstützen.

  • Verwendung von case in gefolgt von einer durch Kommas getrennten Liste
    case in 401, 403, 404:
      print("Some HTTP error")
    

    Dies würde nicht für OR-Muster funktionieren, die in andere Muster verschachtelt sind, wie

    case Point(0|1, 0|1):
        print("A corner of the unit square")
    
  • Verwendung des Schlüsselworts or
    case 401 or 403 or 404:
        print("Some HTTP error")
    

    Dies könnte funktionieren, und die Lesbarkeit ist nicht allzu verschieden von der Verwendung von |. Einige Benutzer äußerten eine Präferenz für or, da sie | mit bitweisem OR assoziieren. Jedoch

    1. Viele andere Sprachen, die Musterabgleich haben, verwenden | (die Liste umfasst Elixir, Erlang, F#, Mathematica, OCaml, Ruby, Rust und Scala).
    2. | ist kürzer, was zur Lesbarkeit von verschachtelten Mustern wie Point(0|1, 0|1) beitragen kann.
    3. Einige Leute glauben fälschlicherweise, dass | die falsche Priorität hat; aber da Muster keine anderen Operatoren unterstützen, hat es die gleiche Priorität wie in Ausdrücken.
    4. Python-Benutzer verwenden or sehr häufig und bauen möglicherweise den Eindruck auf, dass es stark mit booleschem Short-Circuiting verbunden ist.
    5. | wird zwischen Alternativen in regulären Ausdrücken und in EBNF-Grammatiken (wie der von Python selbst) verwendet.
    6. | wird nicht nur für bitweises OR verwendet – es wird für Mengenvereinigungen, Dictionary-Zusammenführung (PEP 584) und wird als Alternative zu typing.Union (PEP 604) in Betracht gezogen.
    7. | funktioniert besser als visueller Trenner, insbesondere zwischen Zeichenfolgen. Vergleichen Sie
      case "spam" or "eggs" or "cheese":
      

      zu

      case "spam" | "eggs" | "cheese":
      

Fügen Sie eine else-Klausel hinzu

Wir haben uns aus mehreren Gründen dagegen entschieden, eine else-Klausel hinzuzufügen.

  • Sie ist redundant, da wir bereits case _: haben
  • Es wird immer Verwirrung über die Einrückungsebene von else: geben – sollte sie mit der Liste der Fälle oder mit dem match-Schlüsselwort übereinstimmen?
  • Vollständigkeitsargumente wie „jede andere Anweisung hat eine“ sind falsch – nur diejenigen Anweisungen haben eine else-Klausel, bei denen sie neue Funktionalität hinzufügt.

Zurückgestellte Ideen

Es gab eine Reihe von Vorschlägen zur Erweiterung der Abgleichsyntax, die wir für eine mögliche zukünftige PEP zurückstellen beschlossen haben. Diese fallen in den Bereich „coole Idee, aber nicht essentiell“, und es wurde das Gefühl vermittelt, dass es besser sein könnte, einige reale Daten darüber zu sammeln, wie die Match-Anweisung in der Praxis verwendet wird, bevor man mit einigen dieser Vorschläge fortfährt.

Beachten Sie, dass in jedem Fall die Idee als „Zwei-Wege-Tür“ beurteilt wurde, was bedeutet, dass es keine Rückwärtskompatibilitätsprobleme geben sollte, wenn diese Funktionen später hinzugefügt werden.

Einmalige Syntaxvariante

Bei der Inspektion einiger Codebasen, die am meisten von der vorgeschlagenen Syntax profitieren könnten, wurde festgestellt, dass Einzelsatz-Übereinstimmungen relativ oft verwendet würden, hauptsächlich für verschiedene Sonderfälle. In anderen Sprachen wird dies in Form von One-Off-Matches unterstützt. Wir haben vorgeschlagen, solche One-Off-Matches ebenfalls zu unterstützen

if match value as pattern [and guard]:
    ...

oder alternativ ohne das if

match value as pattern [if guard]:
    ...

als äquivalent zur folgenden Erweiterung

match value:
    case pattern [if guard]:
        ...

Um zu veranschaulichen, wie dies die Lesbarkeit verbessert, betrachten Sie diesen (leicht vereinfachten) Ausschnitt aus echtem Code

if isinstance(node, CallExpr):
    if (isinstance(node.callee, NameExpr) and len(node.args) == 1 and
            isinstance(node.args[0], NameExpr)):
        call = node.callee.name
        arg = node.args[0].name
        ...  # Continue special-casing 'call' and 'arg'
...  # Follow with common code

Dies kann auf eine geradlinigere Weise umgeschrieben werden als

if match node as CallExpr(callee=NameExpr(name=call), args=[NameExpr(name=arg)]):
    ...  # Continue special-casing 'call' and 'arg'
...  # Follow with common code

Diese One-Off-Form würde keine elif match-Anweisungen zulassen, da sie nur für einen einzelnen Musterfall bestimmt war. Sie war als Sonderfall einer match-Anweisung gedacht, nicht als Sonderfall einer if-Anweisung

if match value_1 as patter_1 [and guard_1]:
    ...
elif match value_2 as pattern_2 [and guard_2]:  # Not allowed
    ...
elif match value_3 as pattern_3 [and guard_3]:  # Not allowed
    ...
else:  # Also not allowed
    ...

Dies würde den Zweck von One-Off-Matches als Ergänzung zu erschöpfenden vollständigen Matches vereiteln – es ist besser und klarer, in diesem Fall ein vollständiges Match zu verwenden.

Ebenso wäre if not match nicht zulässig, da match ... as ... keine Expression ist. Wir schlagen auch keinen while match-Konstrukt vor, das in einigen Sprachen mit Musterabgleich vorhanden ist, da es zwar praktisch sein mag, aber wahrscheinlich selten verwendet wird.

Andere musterbasierte Konstrukte

Viele andere Sprachen, die Musterabgleich unterstützen, nutzen ihn als Grundlage für mehrere Sprachkonstrukte, einschließlich eines Abgleichoperators, einer verallgemeinerten Form der Zuweisung, eines Filters für Schleifen, einer Methode zur Synchronisierung der Kommunikation oder spezialisierter If-Anweisungen. Einige davon wurden in der Diskussion des ersten Entwurfs erwähnt. Eine weitere gestellte Frage war, warum diese spezielle Form (die Bindung und bedingte Auswahl kombiniert) gewählt wurde, während andere Formen nicht gewählt wurden.

Die Einführung weiterer Verwendungen von Mustern wäre zu kühn und verfrüht angesichts unserer Erfahrung mit Mustern und würde diesen Vorschlag zu kompliziert machen. Die dargestellte Anweisung bietet eine Form des Features, die ausreichend allgemein ist, um nützlich zu sein, während sie in sich abgeschlossen ist und keine massiven Auswirkungen auf die Syntax und Semantik der Sprache als Ganzes hat.

Nach einiger Erfahrung mit diesem Feature könnte die Community ein besseres Gefühl dafür bekommen, welche anderen Verwendungen von Musterabgleichen in Python wertvoll sein könnten.

Algebraisches Matching wiederholter Namen

Eine Technik, die gelegentlich in funktionalen Sprachen wie Erlang und Elixir zu sehen ist, ist die mehrfache Verwendung einer Match-Variable im selben Muster

match value:
    case Point(x, x):
        print("Point is on a diagonal!")

Die Idee hier ist, dass die erste Erscheinung von x den Wert an den Namen binden würde und nachfolgende Vorkommen überprüfen würden, ob der eingehende Wert gleich dem zuvor gebundenen Wert war. Wenn der Wert nicht gleich war, würde der Abgleich fehlschlagen.

Es gibt jedoch eine Reihe von Feinheiten bei der Vermischung von Lade- und Speichersemantik für Capture-Muster. Vorerst haben wir beschlossen, die wiederholte Verwendung von Namen innerhalb desselben Musters als Fehler zu behandeln; wir können diese Einschränkung später immer noch aufheben, ohne die Rückwärtskompatibilität zu beeinträchtigen.

Beachten Sie, dass Sie denselben Namen mehrmals in alternativen Auswahlmöglichkeiten *verwenden können*

match value:
    case x | [x]:
        # etc.

Benutzerdefiniertes Matching-Protokoll

Während der anfänglichen Design-Diskussionen für diese PEP wurden viele Ideen zu benutzerdefinierten Matchern herumgeworfen. Es gab ein paar Motivationen dafür

  • Einige Klassen möchten möglicherweise einen anderen Satz von „abgleichbaren“ Namen als die tatsächlichen Klasseneigenschaften preisgeben.
  • Einige Klassen haben möglicherweise Eigenschaften, die teuer zu berechnen sind und daher nicht ausgewertet werden sollten, es sei denn, das Muster benötigt tatsächlich Zugriff darauf.
  • Es gab Ideen für exotische Matcher wie IsInstance(), InRange(), RegexMatchingGroup() und so weiter.
  • Damit integrierte Typen und Standardbibliotheksklassen den Abgleich auf eine vernünftige und intuitive Weise unterstützen können, wurde angenommen, dass diese Typen eine spezielle Abgleichlogik implementieren müssten.

Diese angepassten Abgleichverhalten würden von einer speziellen Methode __match__ am Klassennamen gesteuert. Es gab zwei konkurrierende Varianten

  • Ein „voll ausgestattetes“ Abgleichprotokoll, das nicht nur das abzugleichende Subjekt übergibt, sondern auch detaillierte Informationen darüber, an welchen Attributen das angegebene Muster interessiert war.
  • Ein vereinfachtes Abgleichprotokoll, das nur den Subjektwert übergibt und ein „Proxy-Objekt“ zurückgibt (das in den meisten Fällen das Subjekt selbst sein könnte), das die abgleichbaren Attribute enthält.

Hier ist ein Beispiel für eine Version des komplexeren vorgeschlagenen Protokolls

match expr:
    case BinaryOp(left=Number(value=x), op=op, right=Number(value=y)):
        ...

from types import PatternObject
BinaryOp.__match__(
    (),
    {
        "left": PatternObject(Number, (), {"value": ...}, -1, False),
        "op": ...,
        "right": PatternObject(Number, (), {"value": ...}, -1, False),
    },
    -1,
    False,
)

Ein Nachteil dieses Protokolls ist, dass die Argumente für __match__ teuer zu erstellen wären und nicht vortrainiert werden könnten, da es aufgrund der Art und Weise, wie Namen gebunden werden, keine echten Konstanten in Python gibt. Es bedeutete auch, dass die Methode __match__ viel von der Logik des Abgleichs neu implementieren müsste, die ansonsten in C-Code in der Python VM implementiert wäre. Infolgedessen würde diese Option im Vergleich zu einer äquivalenten if-Anweisung schlecht abschneiden.

Das einfachere Protokoll litt unter der Tatsache, dass es zwar performanter war, aber viel weniger flexibel und viele der kreativen benutzerdefinierten Matcher, von denen die Leute träumten, nicht zuließ.

Spät im Designprozess wurde jedoch erkannt, dass der Bedarf an einem benutzerdefinierten Abgleichprotokoll viel geringer war als erwartet. Praktisch alle realistischen (im Gegensatz zu fantastischen) aufgeworfenen Anwendungsfälle konnten mit dem integrierten Abgleichverhalten behandelt werden, obwohl in einigen Fällen eine zusätzliche Guard-Bedingung erforderlich war, um den gewünschten Effekt zu erzielen.

Darüber hinaus stellte sich heraus, dass keine der Standardbibliotheksklassen außer einer geeigneten Eigenschaft __match_args__ eine spezielle Abgleichunterstützung benötigte.

Die Entscheidung, dieses Feature zu verschieben, ging mit der Erkenntnis einher, dass dies keine Einbahnstraße ist; dass ein flexibleres und anpassbareres Abgleichprotokoll später hinzugefügt werden kann, insbesondere wenn wir mehr Erfahrung mit realen Anwendungsfällen und tatsächlichen Benutzerbedürfnissen gewinnen.

Die Autoren dieser PEP erwarten, dass die match-Anweisung im Laufe der Zeit mit sich entwickelnden Nutzungsmustern und Idiomen weiterentwickelt wird, ähnlich wie es andere „mehrstufige“ PEPs in der Vergangenheit getan haben. Wenn dies geschieht, kann die erweiterte Abgleichfrage erneut aufgegriffen werden.

Parametrisierte Matching-Syntax

(Auch bekannt als „Class Instance Matchers“.)

Dies ist eine weitere Variante der Idee „benutzerdefinierte Match-Klassen“, die verschiedene Arten von benutzerdefinierten Matchern ermöglichen würde, die im vorherigen Abschnitt erwähnt wurden – anstatt jedoch ein erweitertes Abgleichprotokoll zu verwenden, würde dies durch die Einführung eines zusätzlichen Mustertyps mit eigener Syntax erreicht werden. Dieser Mustertyp würde zwei verschiedene Parametersätze akzeptieren: ein Satz, der aus den tatsächlichen Parametern besteht, die an den Konstruktor des Musterobjekts übergeben werden, und ein weiterer Satz, der die Bindungsvariablen für das Muster darstellt.

Die __match__-Methode dieser Objekte könnte die Konstruktorparameterwerte verwenden, um zu entscheiden, was ein gültiger Abgleich war.

Dies würde Muster wie InRange<0, 6>(value) ermöglichen, die eine Zahl im Bereich 0..6 abgleichen und den abgeglichenen Wert 'value' zuweisen würden. Ähnlich könnte man ein Muster haben, das die Existenz einer benannten Gruppe in einem regulären Ausdrucksabgleichsergebnis testet (andere Bedeutung des Wortes „match“).

Obwohl es einige Unterstützung für diese Idee gibt, gab es viel „Bikeshedding“ bei der Syntax (es gibt nicht viele attraktive Optionen) und es wurde kein klarer Konsens erzielt, so dass beschlossen wurde, dass dieses Feature für den Moment nicht essentiell für die PEP ist.

Muster-Utility-Bibliothek

Beide vorherigen Ideen würden von einem neuen Python-Standardbibliotheksmodul begleitet, das eine reichhaltige Sammlung nützlicher Matcher enthalten würde. Es ist jedoch nicht wirklich möglich, eine solche Bibliothek zu implementieren, ohne eine der in den vorherigen Abschnitten gegebenen erweiterten Musterungsvorschläge zu übernehmen, so dass auch diese Idee verschoben wird.

Danksagungen

Wir sind dankbar für die Hilfe der folgenden Personen (unter vielen anderen), die uns in verschiedenen Phasen des Schreibens dieser PEP geholfen haben

  • Gregory P. Smith
  • Jim Jewett
  • Mark Shannon
  • Nate Lust
  • Taine Zhao

Versionshistorie

  1. Anfangsversion
  2. Umfangreiche Überarbeitung, einschließlich
    • Kleine Klarstellungen, Grammatik- und Tippfehlerkorrekturen
    • Umbenennung verschiedener Konzepte
    • Zusätzliche Diskussion abgelehnter Ideen, einschließlich
      • Warum wir _ für Platzhalter-Muster wählen
      • Warum wir | für OR-Muster wählen
      • Warum wir keine spezielle Syntax für Capture-Variablen wählen
      • Warum diese Musterabgleichoperation und nicht andere
    • Klarstellung der Ausnahme- und Nebenwirkungssemantik
    • Klarstellung der partiellen Bindungssemantik
    • Beschränkung der Verwendung von _ in Ladekontexten aufheben
    • Das Standard-Argument, das den gesamten Subjekt außer einer Handvoll eingebauter Typen darstellt, aufheben
    • Vereinfachung des Verhaltens von __match_args__
    • Das __match__-Protokoll aufheben (verschoben zu verschobene Ideen)
    • ImpossibleMatchError-Ausnahme aufheben
    • Führenden Punkt für Ladevorgänge aufheben (verschoben zu verschobene Ideen)
    • Die anfänglichen Abschnitte überarbeitet (alles vor Syntax)
    • Eine Übersicht aller Musterarten vor der detaillierten Beschreibung hinzugefügt
    • Vereinfachte Syntax neben der Beschreibung jedes Musters hinzugefügt
    • Beschreibung des Wildcards von Capture-Mustern getrennt
    • Daniel F Moisset als sechsten Co-Autor hinzugefügt

Referenzen

Anhang A – Vollständige Grammatik

Hier ist die vollständige Grammatik für match_stmt. Dies ist eine zusätzliche Alternative für compound_stmt. Es sollte verstanden werden, dass match und case weiche Schlüsselwörter sind, d. h. sie sind in anderen grammatikalischen Kontexten keine reservierten Wörter (einschließlich am Anfang einer Zeile, wenn kein Doppelpunkt erwartet wird). Per Konvention werden harte Schlüsselwörter in einfachen Anführungszeichen und weiche Schlüsselwörter in doppelten Anführungszeichen verwendet.

Andere verwendete Notation jenseits von Standard-EBNF

  • SEP.RULE+ ist eine Abkürzung für RULE (SEP RULE)*
  • !RULE ist eine negative Lookahead-Assertion
match_expr:
    | star_named_expression ',' star_named_expressions?
    | named_expression
match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
patterns: value_pattern ',' [values_pattern] | pattern
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: '|'.closed_pattern+
closed_pattern:
    | capture_pattern
    | literal_pattern
    | constant_pattern
    | group_pattern
    | sequence_pattern
    | mapping_pattern
    | class_pattern
capture_pattern: NAME !('.' | '(' | '=')
literal_pattern:
    | signed_number !('+' | '-')
    | signed_number '+' NUMBER
    | signed_number '-' NUMBER
    | strings
    | 'None'
    | 'True'
    | 'False'
constant_pattern: attr !('.' | '(' | '=')
group_pattern: '(' patterns ')'
sequence_pattern: '[' [values_pattern] ']' | '(' ')'
mapping_pattern: '{' items_pattern? '}'
class_pattern:
    | name_or_attr '(' ')'
    | name_or_attr '(' ','.pattern+ ','? ')'
    | name_or_attr '(' ','.keyword_pattern+ ','? ')'
    | name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
signed_number: NUMBER | '-' NUMBER
attr: name_or_attr '.' NAME
name_or_attr: attr | NAME
values_pattern: ','.value_pattern+ ','?
items_pattern: ','.key_value_pattern+ ','?
keyword_pattern: NAME '=' or_pattern
value_pattern: '*' capture_pattern | pattern
key_value_pattern:
    | (literal_pattern | constant_pattern) ':' or_pattern
    | '**' capture_pattern

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

Zuletzt geändert: 2025-02-01 08:55:40 GMT