PEP 635 – Strukturelle Mustererkennung: Motivation und Begründung
- Autor:
- Tobias Kohn <kohnt at tobiaskohn.ch>, 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
Zusammenfassung
Dieses PEP liefert die Motivation und Begründung für PEP 634 („Strukturelle Mustererkennung: Spezifikation“). Erstlesern wird empfohlen, mit PEP 636 zu beginnen, das eine sanftere Einführung in die Konzepte, Syntax und Semantik von Mustern bietet.
Motivation
(Strukturelle) Mustererkennungssyntax ist in vielen Sprachen zu finden, von Haskell, Erlang und Scala bis Elixir und Ruby. (Ein Vorschlag für JavaScript wird ebenfalls geprüft.)
Python unterstützt bereits eine eingeschränkte Form davon durch Zuweisungen mit Sequenzentpackung, die der neue Vorschlag nutzt.
Mehrere andere gängige Python-Idiome sind ebenfalls relevant
- Das Idiom
if ... elif ... elif ... elsewird häufig verwendet, um auf ad-hoc-Weise den Typ oder die Struktur eines Objekts zu ermitteln, wobei eine oder mehrere Prüfungen wieisinstance(x, cls),hasattr(x, "attr"),len(x) == noder"key" in xals Schutzbedingungen verwendet werden, um einen anwendbaren Block auszuwählen. Der Block kann dann davon ausgehen, dassxdie von der Schutzbedingung geprüfte Schnittstelle unterstützt. Zum Beispielif isinstance(x, tuple) and len(x) == 2: host, port = x mode = "http" elif isinstance(x, tuple) and len(x) == 3: host, port, mode = x # Etc.
Code wie dieser wird mit
matcheleganter dargestellt.match x: case host, port: mode = "http" case host, port, mode: pass # Etc.
- Code zur AST-Durchquerung sucht oft nach Knoten, die einem bestimmten Muster entsprechen. Zum Beispiel könnte der Code, der einen Knoten mit der Struktur „A + B * C“ erkennt, so aussehen:
if (isinstance(node, BinOp) and node.op == "+" and isinstance(node.right, BinOp) and node.right.op == "*"): a, b, c = node.left, node.right.left, node.right.right # Handle a + b*c
Mit
matchwird dies lesbarer.match node: case BinOp("+", a, BinOp("*", b, c)): # Handle a + b*c
Wir glauben, dass die Hinzufügung der Mustererkennung zu Python es Python-Benutzern ermöglichen wird, saubereren, besser lesbaren Code für die obigen Beispiele und viele andere zu schreiben.
Eine akademischere Diskussion zu diesem Vorschlag finden Sie unter [1].
Mustererkennung und OO
Mustererkennung ist komplementär zum objektorientierten Paradigma. Mit OO und Vererbung können wir problemlos eine Methode in einer Basisklasse definieren, die Standardverhalten für eine bestimmte Operation dieser Klasse festlegt, und dieses Standardverhalten in Unterklassen überschreiben. Wir können auch das Visitor-Muster verwenden, um Aktionen von Daten zu trennen.
Aber das ist für alle Situationen nicht ausreichend. Beispielsweise kann ein Codegenerator ein AST verarbeiten und viele Operationen haben, bei denen der generierte Code nicht nur von der Klasse eines Knotens, sondern auch vom Wert einiger Klassenattribute abhängen muss, wie im obigen BinOp-Beispiel. Das Visitor-Muster ist dafür nicht flexibel genug: Es kann nur basierend auf der Klasse auswählen.
Siehe ein vollständiges Beispiel.
Ähnlich wie das Visitor-Muster ermöglicht die Mustererkennung eine strikte Trennung von Belangen: spezifische Aktionen oder Datenverarbeitung sind unabhängig von der Klassenhierarchie oder den bearbeiteten Objekten. Insbesondere bei vordefinierten oder sogar integrierten Klassen ist es oft unmöglich, weitere Methoden zu den einzelnen Klassen hinzuzufügen. Mustererkennung entlastet den Programmierer oder Klassendesigner nicht nur von der Boilerplate-Code, die für das Visitor-Muster benötigt wird, sondern ist auch flexibel genug, um direkt mit integrierten Typen zu arbeiten. Sie unterscheidet natürlich zwischen Sequenzen unterschiedlicher Länge, die sich trotz offensichtlich unterschiedlicher Strukturen die gleiche Klasse teilen können. Darüber hinaus berücksichtigt die Mustererkennung automatisch die Vererbung: Eine Klasse D, die von C erbt, wird standardmäßig von einem Muster behandelt, das auf C abzielt.
Objektorientierte Programmierung ist auf Single-Dispatch ausgelegt: Eine einzelne Instanz (oder ihr Typ) bestimmt, welche Methode aufgerufen werden soll. Dies führt zu einer etwas künstlichen Situation bei binären Operatoren, bei denen beide Objekte eine gleichberechtigte Rolle bei der Entscheidung spielen könnten, welche Implementierung verwendet werden soll (Python löst dies durch die Verwendung umgekehrter binärer Methoden). Mustererkennung ist strukturell besser geeignet, um solche Situationen des Multi-Dispatches zu handhaben, bei denen die auszuführende Aktion von den Typen mehrerer Objekte gleichermaßen abhängt.
Muster und funktionaler Stil
Viele Python-Anwendungen und Bibliotheken sind nicht im konsistenten OO-Stil geschrieben – im Gegensatz zu Java ermutigt Python dazu, Funktionen auf der obersten Ebene eines Moduls zu definieren, und für einfache Datenstrukturen werden Tupel (oder benannte Tupel oder Listen) und Wörterbücher oft ausschließlich oder gemischt mit Klassen oder Datenklassen verwendet.
Mustererkennung eignet sich besonders gut zum Zerlegen solcher Datenstrukturen. Als extremes Beispiel ist es einfach, Code zu schreiben, der eine JSON-Datenstruktur mit match zerlegt.
match json_pet:
case {"type": "cat", "name": name, "pattern": pattern}:
return Cat(name, pattern)
case {"type": "dog", "name": name, "breed": breed}:
return Dog(name, breed)
case _:
raise ValueError("Not a suitable pet")
Funktionale Programmierung bevorzugt im Allgemeinen einen deklarativen Stil mit Fokus auf Beziehungen in Daten. Nebeneffekte werden, wo immer möglich, vermieden. Mustererkennung passt somit natürlich und unterstützt den funktionalen Programmierstil in hohem Maße.
Begründung
Dieser Abschnitt liefert die Begründung für einzelne Designentscheidungen. Er ersetzt die „Abgelehnte Ideen“ im Standard-PEP-Format. Er ist in Abschnitte unterteilt, die der Spezifikation (PEP 634) entsprechen.
Überblick und Terminologie
Ein Großteil der Leistungsfähigkeit der Mustererkennung ergibt sich aus der Verschachtelung von Teilmustern. Dass der Erfolg einer Mustererkennung direkt vom Erfolg eines Teilmusters abhängt, ist somit ein Eckpfeiler des Designs. Obwohl jedoch ein Muster wie P(Q(), R()) nur dann erfolgreich ist, wenn beide Teilmuster Q() und R() erfolgreich sind (d. h. der Erfolg des Musters P hängt von Q und R ab), wird P zuerst geprüft. Wenn P fehlschlägt, werden weder Q() noch R() versucht (dies ist eine direkte Folge der Tatsache, dass, wenn P fehlschlägt, ohnehin keine Subjekte vorhanden sind, gegen die Q() und R() abgeglichen werden können).
Beachten Sie auch, dass Muster Namen an Werte binden, anstatt eine Zuweisung vorzunehmen. Dies spiegelt die Tatsache wider, dass Muster darauf abzielen, keine Nebeneffekte zu haben, was auch bedeutet, dass Erfassungs- oder AS-Muster keinen Wert in ein Attribut oder einen Unterindex zuweisen können. Wir verwenden daher konsequent den Begriff „binden“ anstelle von „zuweisen“, um diesen subtilen Unterschied zwischen traditionellen Zuweisungen und Namensbindung in Mustern zu betonen.
Die Match-Anweisung
Die match-Anweisung wertet einen Ausdruck aus, um ein Subjekt zu erzeugen, findet das erste Muster, das mit dem Subjekt übereinstimmt, und führt den zugehörigen Codeblock aus. Syntaktisch nimmt die match-Anweisung daher einen Ausdruck und eine Sequenz von case-Klauseln entgegen, wobei jede case-Klausel ein Muster und einen Codeblock umfasst.
Da case-Klauseln die Struktur einer zusammengesetzten Anweisung umfassen, entsprechen sie dem bestehenden Einrückungsschema mit der syntaktischen Struktur von <keyword> ...: <(eingerückter) block>, was einer zusammengesetzten Anweisung ähnelt. Das Schlüsselwort case spiegelt seine weit verbreitete Verwendung in Mustererkennungssprachen wider, wobei Sprachen ignoriert werden, die andere syntaktische Mittel wie ein Symbol wie | verwenden, da dies nicht in etablierte Python-Strukturen passen würde. Die Syntax von Mustern nach dem Schlüsselwort wird unten diskutiert.
Da die case-Klauseln der Struktur einer zusammengesetzten Anweisung folgen, wird die match-Anweisung selbst natürlich ebenfalls zu einer zusammengesetzten Anweisung, die derselben syntaktischen Struktur folgt. Dies führt natürlich zu match <expr>: <case_clause>+. Beachten Sie, dass die match-Anweisung einen Quasi-Gültigkeitsbereich festlegt, in dem das ausgewertete Subjekt am Leben erhalten wird (wenn auch nicht in einer lokalen Variablen), ähnlich wie eine with-Anweisung eine Ressource während der Ausführung ihres Blocks am Leben erhalten könnte. Darüber hinaus fließt die Kontrolle von der match-Anweisung zu einer case-Klausel und verlässt dann den Block der match-Anweisung. Der Block der match-Anweisung hat somit sowohl syntaktische als auch semantische Bedeutung.
Verschiedene Vorschläge versuchten, die natürlich entstehende „doppelte Einrückung“ eines Codeblocks einer case-Klausel zu eliminieren oder zu vermeiden. Leider verstoßen alle solchen Vorschläge für flache Einrückungsschemata gegen das etablierte strukturelle Paradigma von Python und führen zu zusätzlichen syntaktischen Regeln
- Nicht eingerückte case-Klauseln. Die Idee ist, case-Klauseln mit dem
matchauszurichten, d.h.match expression: case pattern_1: ... case pattern_2: ...
Dies mag für das Auge eines Python-Programmierers seltsam aussehen, da überall sonst ein Doppelpunkt gefolgt von einer Einrückung ist. Der
matchwürde weder dem syntaktischen Schema einfacher noch zusammengesetzter Anweisungen folgen, sondern vielmehr eine eigene Kategorie etablieren. - Den Ausdruck auf einer separaten Zeile nach „match“ setzen. Die Idee ist, den Ausdruck, der das Subjekt liefert, als Anweisung zu verwenden, um die Singularität von
matchzu vermeiden, das trotz der Doppelpunkte keinen tatsächlichen Block hat.match: expression case pattern_1: ... case pattern_2: ...
Dies wurde letztendlich abgelehnt, da der erste Block eine weitere Neuheit in der Grammatik von Python wäre: ein Block, dessen einziger Inhalt ein einzelner Ausdruck und keine Sequenz von Anweisungen ist. Versuche, dieses Problem durch Hinzufügen oder Umfunktionieren eines weiteren Schlüsselworts im Sinne von
match: return expressionzu beheben, lieferten keine zufriedenstellende Lösung.
Obwohl eine flache Einrückung horizontalen Platz spart, sind die Kosten für erhöhte Komplexität oder ungewöhnliche Regeln zu hoch. Sie würde auch die Arbeit für einfache Code-Editoren erschweren. Schließlich kann das Problem des horizontalen Platzes durch die Zulassung einer „halben Einrückung“ (d. h. zwei Leerzeichen anstelle von vier) für match-Anweisungen gemildert werden (obwohl wir dies nicht empfehlen).
In Beispielprogrammen, die match verwenden und als Teil der Entwicklung dieses PEP geschrieben wurden, wird eine spürbare Verbesserung der Code-Kürze beobachtet, die die zusätzliche Einrückungsebene mehr als ausgleicht.
Anweisung vs. Ausdruck. Einige Vorschläge konzentrierten sich auf die Idee, match zu einem Ausdruck anstatt zu einer Anweisung zu machen. Dies würde jedoch schlecht zur Anweisungsorientierung von Python passen und zu ungewöhnlich langen und komplexen Ausdrücken führen und die Notwendigkeit erfordern, neue syntaktische Konstrukte zu erfinden oder gut etablierte syntaktische Regeln zu brechen. Eine offensichtliche Konsequenz von match als Ausdruck wäre, dass case-Klauseln keine beliebigen Codeblöcke mehr, sondern nur noch einen einzigen Ausdruck haben könnten. Insgesamt könnten die starken Einschränkungen die leichte Vereinfachung in einigen speziellen Anwendungsfällen in keiner Weise ausgleichen.
Hard vs. Soft Keyword. 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 einfache Syntax-Highlighter vereinfachen würde, haben wir uns aus mehreren Gründen gegen die Verwendung eines Hard Keywords entschieden
- Am wichtigsten ist, dass der neue Parser dies nicht erfordert. Im Gegensatz zu
async, das für einige Releases Probleme mit einem Soft Keyword verursachte, können wir hiermatchzu einem permanenten Soft Keyword machen. matchist in vorhandenem Code so gebräuchlich, dass es fast jedes bestehende Programm brechen würde und eine Belastung zur Fehlerbehebung für viele Personen darstellen würde, die möglicherweise nicht einmal von der neuen Syntax profitieren.- Es ist schwierig, ein alternatives Schlüsselwort zu finden, das nicht bereits als Bezeichner in bestehenden Programmen verwendet wird und dennoch die Bedeutung der Anweisung klar widerspiegelt.
Verwendung von „as“ oder „|“ anstelle von „case“ für case-Klauseln. Die hier vorgeschlagene Mustererkennung ist eine Kombination aus Mehrzweig-Steuerfluss (im Einklang mit switch in Algol-basierten 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 ebenso möglich und würden den Dekonstruktionsaspekt hervorheben. as oder with haben beispielsweise auch den Vorteil, dass sie in Python bereits Schlüsselwörter 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 und als Variable zu unterscheiden.
Andere Varianten würden ein Symbol wie | oder => verwenden oder ganz ohne speziellen Marker auskommen.
Da Python eine anweisungsorientierte Sprache in der Tradition von Algol ist und jede zusammengesetzte Anweisung mit einem identifizierenden Schlüsselwort beginnt, schien case am besten mit dem Stil und den Traditionen von Python übereinzustimmen.
Match-Semantik
Die Muster verschiedener case-Klauseln können sich überschneiden, sodass mehr als eine case-Klausel auf ein gegebenes Subjekt zutrifft. Die First-to-Match-Regel stellt sicher, dass die Auswahl einer case-Klausel für ein gegebenes Subjekt eindeutig ist. Darüber hinaus können case-Klauseln zunehmend allgemeinere Muster aufweisen, die breitere Mengen von Subjekten abdecken. Die First-to-Match-Regel stellt dann sicher, dass das präziseste Muster ausgewählt werden kann (obwohl es in der Verantwortung des Programmierers liegt, die case-Klauseln korrekt zu ordnen).
In einer statisch typisierten Sprache würde die match-Anweisung zu einem Entscheidungsbaum kompiliert, um ein passendes Muster schnell und sehr effizient auszuwählen. Dies würde jedoch erfordern, dass alle Muster rein deklarativ und statisch sind, was gegen die etablierte dynamische Semantik von Python verstößt. Die vorgeschlagene Semantik stellt somit einen Weg dar, das Beste aus beiden Welten zu vereinen: Muster werden in streng sequenzieller Reihenfolge ausprobiert, sodass jede case-Klausel eine tatsächliche Anweisung darstellt. Gleichzeitig erlauben wir dem Interpreter, Informationen über das Subjekt zwischenzuspeichern oder die Reihenfolge zu ändern, in der Teilmuster ausprobiert werden. Mit anderen Worten: Wenn der Interpreter festgestellt hat, dass das Subjekt keine Instanz der Klasse C ist, kann er direkt case-Klauseln überspringen, die dies erneut prüfen, ohne wiederholte Instanzprüfungen durchführen zu müssen. Wenn eine Schutzbedingung vorschreibt, dass eine Variable x positiv sein muss, z. B. (d. h. if x > 0), kann der Interpreter dies direkt nach der Bindung von x und vor der Berücksichtigung weiterer Teilmuster prüfen.
Bindung und Gültigkeitsbereich. In vielen Implementierungen von Mustererkennungen würde jede case-Klausel einen eigenen separaten Gültigkeitsbereich erstellen. Von einem Muster gebundene Variablen wären dann nur innerhalb des entsprechenden Case-Blocks sichtbar. In Python ergibt dies jedoch keinen Sinn. Die Erstellung separater Gültigkeitsbereiche würde im Wesentlichen bedeuten, dass jede case-Klausel eine separate Funktion ist, ohne direkten Zugriff auf die Variablen im umgebenden Gültigkeitsbereich (ohne auf nonlocal zurückgreifen zu müssen). Darüber hinaus könnte eine case-Klausel keinen umgebenden Kontrollfluss über Standardanweisungen wie return oder break beeinflussen. Daher würde eine solche strenge Gültigkeitsbereichsregelung zu unintuitiven und überraschenden Verhaltensweisen führen.
Eine direkte Folge davon ist, dass alle Variablenbindungen die jeweilige case- oder match-Anweisung überdauern. Selbst Muster, die ein Subjekt nur teilweise abgleichen, können lokale Variablen binden (dies ist tatsächlich notwendig, damit Schutzbedingungen ordnungsgemäß funktionieren). Diese Semantik für Variablenbindung steht jedoch im Einklang mit bestehenden Python-Strukturen wie for-Schleifen und with-Anweisungen.
Guards
Einige Einschränkungen lassen sich durch Muster allein nicht angemessen ausdrücken. Beispielsweise widerspricht eine „kleiner als“- oder „größer als“-Beziehung der üblichen „gleich“-Semantik von Mustern. Darüber hinaus sind verschiedene Teilmuster unabhängig und können nicht aufeinander verweisen. Die Hinzufügung von Schutzbedingungen adressiert diese Einschränkungen: Eine Schutzbedingung ist ein beliebiger Ausdruck, der an ein Muster angehängt ist und zu einem „wahrheitsgemäßen“ Wert ausgewertet werden muss, damit das Muster erfolgreich ist.
Zum Beispiel verwendet case [x, y] if x < y: eine Schutzbedingung (if x < y), um eine „kleiner als“-Beziehung zwischen zwei ansonsten getrennten Erfassungsmustern x und y auszudrücken.
Aus konzeptioneller Sicht beschreiben Muster strukturelle Einschränkungen des Subjekts in einem deklarativen Stil, idealerweise ohne Nebeneffekte. Denken Sie insbesondere daran, dass Muster klar von Ausdrücken zu unterscheiden sind und unterschiedlichen Zielen und Semantiken folgen. Schutzbedingungen erweitern dann Case-Blöcke auf hochkontrollierte Weise mit beliebigen Ausdrücken (die Nebeneffekte haben könnten). Die Aufteilung der Gesamtfunktionalität in einen statischen strukturellen und einen dynamisch ausgewerteten Teil hilft nicht nur bei der Lesbarkeit, sondern kann auch ein dramatisches Potenzial für Compiler-Optimierungen eröffnen. Um diese klare Trennung beizubehalten, werden Schutzbedingungen nur auf der Ebene von Case-Klauseln und nicht für einzelne Muster unterstützt.
Beispiel mit Schutzbedingungen
def sort(seq):
match seq:
case [] | [_]:
return seq
case [x, y] if x <= y:
return seq
case [x, y]:
return [y, x]
case [x, y, z] if x <= y <= z:
return seq
case [x, y, z] if x >= y >= z:
return [z, y, x]
case [p, *rest]:
a = sort([x for x in rest if x <= p])
b = sort([x for x in rest if p < x])
return a + [p] + b
Muster
Muster erfüllen zwei Zwecke: Sie legen (strukturelle) Einschränkungen für das Subjekt fest und sie spezifizieren, welche Datenwerte aus dem Subjekt extrahiert und an Variablen gebunden werden sollen. Bei iterierbaren Entpackungen, die als Prototyp für die Mustererkennung in Python angesehen werden können, gibt es nur ein strukturelles Muster, um Sequenzen auszudrücken, während es eine reiche Auswahl an Bindungsmustern gibt, um einen Wert einer bestimmten Variable oder einem Feld zuzuweisen. Vollständige Mustererkennung unterscheidet sich davon dadurch, dass es mehr Vielfalt bei strukturellen Mustern gibt, aber nur ein Minimum an Bindungsmustern.
Muster unterscheiden sich von Zuweisungszielen (wie bei iterierbaren Entpackungen) in zweierlei Hinsicht: Sie legen zusätzliche Einschränkungen für die Struktur des Subjekts fest, und ein Subjekt kann es sicher versäumen, zu irgendeinem Zeitpunkt mit einem bestimmten Muster übereinzustimmen (bei iterierbaren Entpackungen stellt dies einen Fehler dar). Letzteres bedeutet, dass Muster Nebeneffekte, wo immer möglich, vermeiden sollten.
Dieser Wunsch, Nebeneffekte zu vermeiden, ist ein Grund dafür, dass Erfassungsmuster keine Bindung von Werten an Attribute oder Unterindizes zulassen: Wenn das enthaltende Muster in einem späteren Schritt fehlschlägt, wäre es schwierig, solche Bindungen rückgängig zu machen.
Ein Eckpfeiler der Mustererkennung ist die Möglichkeit, Muster beliebig zu verschachteln. Die Verschachtelung ermöglicht die Darstellung tiefer Baumstrukturen (für ein Beispiel verschachtelter Klassenmuster siehe den obigen Motivationsabschnitt) sowie Alternativen.
Obwohl Muster oberflächlich betrachtet wie Ausdrücke aussehen mögen, ist es wichtig zu bedenken, dass es eine klare Unterscheidung gibt. Tatsächlich ist kein Muster ein Ausdruck und enthält auch keinen. Es ist produktiver, Muster als deklarative Elemente zu betrachten, ähnlich den formalen Parametern einer Funktionsdefinition.
AS-Muster
Muster fallen in zwei Kategorien: Die meisten Muster legen eine (strukturelle) Einschränkung fest, die das Subjekt erfüllen muss, während das Erfassungsmuster das Subjekt an einen Namen bindet, ohne Rücksicht auf die Struktur oder den tatsächlichen Wert des Subjekts. Folglich kann ein Muster entweder eine Einschränkung ausdrücken oder einen Wert binden, aber nicht beides. AS-Muster füllen diese Lücke, indem sie es dem Benutzer ermöglichen, ein allgemeines Muster anzugeben und das Subjekt in einer Variablen zu erfassen.
Typische Anwendungsfälle für das AS-Muster sind OR- und Klassenmuster zusammen mit einem Bindungsnamen, wie z. B. case BinOp('+'|'-' as op, ...): oder case [int() as first, int() as second]:. Letzteres könnte so verstanden werden, dass das Subjekt zwei verschiedene Muster erfüllen muss: [first, second] sowie [int(), int()]. Das AS-Muster kann somit als Spezialfall eines „and“-Musters betrachtet werden (siehe OR-Muster unten für eine zusätzliche Diskussion von „and“-Mustern).
In einer früheren Version wurde das AS-Muster als „Walrus-Muster“ konzipiert, geschrieben als case [first:=int(), second:=int()]. Die Verwendung von as bietet jedoch einige Vorteile gegenüber :=
- Der Walross-Operator
:=wird verwendet, um das Ergebnis eines Ausdrucks auf der rechten Seite zu erfassen, währendasim Allgemeinen eine Art von „Verarbeitung“ anzeigt, wie inimport foo as baroderexcept E as err:. Tatsächlich weist das MusterP as xnicht das MusterPdem Wertxzu, sondern das Subjekt, das erfolgreich mitPübereinstimmt. asermöglicht einen konsistenteren Datenfluss von links nach rechts (die Attribute in Klassenmustern folgen ebenfalls einem Links-nach-Rechts-Datenfluss).- Der Walross-Operator sieht der Syntax für das Abgleichen von Attributen im Klassenmuster sehr ähnlich, was potenziell zu einigen Verwirrungen führen kann.
Beispiel mit dem AS-Muster
def simplify_expr(tokens):
match tokens:
case [('('|'[') as l, *expr, (')'|']') as r] if (l+r) in ('()', '[]'):
return simplify_expr(expr)
case [0, ('+'|'-') as op, right]:
return UnaryOp(op, right)
case [(int() | float() as left) | Num(left), '+', (int() | float() as right) | Num(right)]:
return Num(left + right)
case [(int() | float()) as value]:
return Num(value)
ODER-Muster
Das OR-Muster ermöglicht es Ihnen, „strukturell äquivalente“ Alternativen zu einem neuen Muster zu kombinieren, d.h. mehrere Muster können einen gemeinsamen Handler teilen. Wenn eines der Teilmuster eines OR-Musters mit dem Subjekt übereinstimmt, ist das gesamte OR-Muster erfolgreich.
Statisch typisierte Sprachen verbieten die Bindung von Namen (Erfassungsmuster) innerhalb eines OR-Musters aufgrund potenzieller Konflikte bezüglich der Variablentypen. Als dynamisch typisierte Sprache kann Python hier weniger restriktiv sein und Erfassungsmuster innerhalb von OR-Mustern zulassen. Jedes Teilmuster muss jedoch die gleiche Menge an Variablen binden, um keine potenziell undefinierten Namen zu hinterlassen. Bei zwei Alternativen P | Q bedeutet dies, dass, wenn P die Variablen u und v bindet, Q genau dieselben Variablen u und v binden muss.
Es gab einige Diskussionen darüber, ob das Pipe-Symbol | oder das Schlüsselwort or zur Trennung von Alternativen verwendet werden soll. Das OR-Muster passt nicht vollständig zur bestehenden Semantik und Verwendung beider Symbole. Das Symbol | ist jedoch in allen Programmiersprachen mit OR-Musterunterstützung die Wahl und wird auch für reguläre Ausdrücke in Python in dieser Kapazität verwendet. Es ist auch der traditionelle Trenner zwischen Alternativen in formalen Grammatiken (einschließlich der von Python). Darüber hinaus wird | nicht nur für bitweises OR, sondern auch für Mengenvereinigung und Wörterbuchzusammenführung (PEP 584) verwendet.
Andere Alternativen wurden ebenfalls in Betracht gezogen, aber keine davon würde es ermöglichen, OR-Muster in andere Muster zu verschachteln
- Verwendung eines Kommas:
case 401, 403, 404: print("Some HTTP error")
Dies sieht einem Tupel zu ähnlich aus – wir müssten eine andere Möglichkeit finden, Tupel zu schreiben, und der Konstrukt müsste innerhalb der Argumentliste eines Klassenmusters in Klammern gesetzt werden. Im Allgemeinen haben Kommas in Python bereits viele verschiedene Bedeutungen, wir sollten keine weiteren hinzufügen.
- Verwendung gestapelter cases:
case 401: case 403: case 404: print("Some HTTP error")
So würde dies in C unter Verwendung seiner Fall-Through-Semantik für cases erfolgen. Wir wollen die Leute jedoch nicht in die Irre führen und denken lassen, dass match/case Fall-Through-Semantik verwendet (was eine häufige Fehlerquelle in C darstellt). Außerdem wäre dies ein neues Einrückungsmuster, was die Unterstützung in IDEs und ähnlichem erschweren könnte (es würde die einfache Regel „füge nach einer Zeile, die mit einem Doppelpunkt endet, eine Einrückungsebene hinzu“ brechen). Schließlich würde dies auch keine OR-Muster unterstützen, die in andere Muster verschachtelt sind.
- Verwendung von „case in“ gefolgt von einer durch Kommas getrennten Liste:
case in 401, 403, 404: print("Some HTTP error")
Dies würde für OR-Muster, die in andere Muster verschachtelt sind, nicht funktionieren, wie zum Beispiel
case Point(0|1, 0|1): print("A corner of the unit square")
AND- und NOT-Muster
Da dieser Vorschlag ein OR-Muster (|) zur Übereinstimmung mit einer von mehreren Alternativen definiert, warum nicht auch ein AND-Muster (&) oder sogar ein NOT-Muster (!)? Insbesondere da einige andere Sprachen (F# zum Beispiel) AND-Muster unterstützen.
Es ist jedoch nicht klar, wie nützlich dies wäre. Die Semantik für die Abgleichung von Wörterbüchern, Objekten und Sequenzen beinhaltet bereits ein implizites „und“: Alle genannten Attribute und Elemente müssen vorhanden sein, damit der Abgleich erfolgreich ist. Schutzbedingungen können auch viele der Anwendungsfälle unterstützen, für die ein hypothetischer „and“-Operator verwendet würde.
Eine Negation eines Abgleichmusters unter Verwendung des Operators ! 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. Es gibt jedoch Hinweise aus anderen Sprachen, dass dies selten nützlich ist und hauptsächlich als doppelte Negation !! zur Steuerung von Variablenbereichen und zur Verhinderung von Variablenbindungen verwendet wird (was für Python nicht zutrifft). Andere Anwendungsfälle werden besser mit Schutzbedingungen ausgedrückt.
Schließlich wurde entschieden, dass dies die Syntax komplexer machen würde, ohne einen signifikanten Vorteil zu bringen. Es kann immer noch später hinzugefügt werden.
Beispiel mit dem OR-Muster
def simplify(expr):
match expr:
case ('/', 0, 0):
return expr
case ('*'|'/', 0, _):
return 0
case ('+'|'-', x, 0) | ('+', 0, x) | ('*', 1, x) | ('*'|'/', x, 1):
return x
return expr
Literale Muster
Literale Muster sind eine bequeme Möglichkeit, Einschränkungen für den Wert eines Subjekts festzulegen, anstatt für seinen Typ oder seine Struktur. Sie ermöglichen es Ihnen auch, eine switch-Anweisung mit Mustererkennung zu emulieren.
Im Allgemeinen wird das Subjekt mittels Standardgleichheit (x == y in Python-Syntax) mit einem literalen Muster verglichen. Folglich passen die literalen Muster 1.0 und 1 genau auf die gleiche Menge von Objekten, d.h. case 1.0: und case 1: sind vollständig austauschbar. Prinzipiell würde True ebenfalls auf die gleiche Menge von Objekten passen, da True == 1 gilt. Wir glauben jedoch, dass viele Benutzer überrascht wären, wenn case True: mit dem Subjekt 1.0 übereinstimmen würde, was zu subtilen Fehlern und umständlichen Workarounds führen würde. Wir haben daher die Regel angenommen, dass die drei Singleton-Muster None, False und True per Identität (x is y in Python-Syntax) und nicht per Gleichheit übereinstimmen. Daher passt case True: nur zu True und nichts anderem. Beachten Sie, dass case 1: zwar immer noch mit True übereinstimmen würde, da das literale Muster 1 nach Gleichheit und nicht nach Identität arbeitet.
Frühe Ideen, eine Hierarchie für Zahlen einzuführen, sodass case 1.0 sowohl mit der Ganzzahl 1 als auch mit der Fließkommazahl 1.0 übereinstimmen würde, während case 1: nur mit der Ganzzahl 1 übereinstimmen würde, wurden schließlich zugunsten der einfacheren und konsistenteren Regel auf Basis von Gleichheit aufgegeben. Darüber hinaus würden zusätzliche Prüfungen, ob das Subjekt eine Instanz von numbers.Integral ist, zu hohen Laufzeitkosten führen, um etwas einzuführen, das im Wesentlichen eine Neuheit in Python wäre. Bei Bedarf kann die explizite Syntax case int(1): verwendet werden.
Wir erinnern daran, dass literale Muster *keine* Ausdrücke sind, sondern direkt einen spezifischen Wert bezeichnen. Aus pragmatischer Sicht möchten wir die Verwendung von negativen und sogar komplexen Werten als literale Muster erlauben, aber sie sind keine atomaren Literale (nur vorzeichenlose reelle und imaginäre Zahlen sind es). Z.B. ist -3+4j syntaktisch ein Ausdruck der Form BinOp(UnaryOp('-', 3), '+', 4j). Da Ausdrücke keine Bestandteile von Mustern sind, mussten wir explizite syntaktische Unterstützung für solche Werte hinzufügen, ohne auf vollständige Ausdrücke zurückgreifen zu müssen.
Interpolierte f-Strings sind dagegen keine literalen Werte, trotz ihres Aussehens, und können daher nicht als literale Muster verwendet werden (String-Verkettung wird jedoch unterstützt).
Literale Muster treten nicht nur als eigene Muster auf, sondern auch als Schlüssel in Mapping-Mustern.
Bereichsabgleichsmuster. Dies würde Muster wie 1...6 erlauben. Es gibt jedoch eine Vielzahl von Mehrdeutigkeiten
- Ist der Bereich offen, halb-offen oder geschlossen? (D.h. ist
6im obigen Beispiel enthalten oder nicht?) - Passt der Bereich zu einer einzelnen Zahl oder einem Bereichsobjekt?
- Bereichsabgleiche werden oft für Zeichenbereiche verwendet ('a'...'z'), aber das funktioniert in Python nicht, da es keinen Zeichendatentyp gibt, nur Strings.
- Bereichsabgleiche können eine signifikante Leistungsoptimierung sein, wenn man eine Sprungtabelle vorab erstellen kann, aber das ist in Python generell nicht möglich, da Namen dynamisch neu gebunden werden können.
Anstatt eine spezielle Syntax für Bereiche zu erstellen, wurde beschlossen, dass die Erlaubnis von benutzerdefinierten Musterobjekten (InRange(0, 6)) flexibler und weniger mehrdeutig wäre; diese Ideen wurden jedoch vorerst zurückgestellt.
Beispiel für literale Muster
def simplify(expr):
match expr:
case ('+', 0, x):
return x
case ('+' | '-', x, 0):
return x
case ('and', True, x):
return x
case ('and', False, x):
return False
case ('or', False, x):
return x
case ('or', True, x):
return True
case ('not', ('not', x)):
return x
return expr
Erfassungsmuster
Capture-Muster nehmen die Form eines Namens an, der jeden Wert akzeptiert und ihn an eine (lokale) Variable bindet (es sei denn, der Name ist als nonlocal oder global deklariert). In diesem Sinne ähnelt ein Capture-Muster einem Parameter in einer Funktionsdefinition (wenn die Funktion aufgerufen wird, bindet jeder Parameter das entsprechende Argument an eine lokale Variable im Geltungsbereich der Funktion).
Ein für ein Capture-Muster verwendeter Name darf nicht mit einem anderen Capture-Muster im selben Muster übereinstimmen. Dies ist wiederum ähnlich wie bei Parametern, die ebenfalls erfordern, dass jeder Parameternamen innerhalb der Liste der Parameter eindeutig ist. Es unterscheidet sich jedoch von der Zuweisung durch iterierbares Entpacken, bei der die wiederholte Verwendung eines Variablennamens als Ziel zulässig ist (z.B. x, x = 1, 2). Die Begründung für die Nichtunterstützung von (x, x) in Mustern ist seine mehrdeutige Lesart: Es könnte als iterierbares Entpacken angesehen werden, bei dem nur die zweite Bindung an x überlebt. Es könnte aber ebenso als Ausdruck eines Tupels mit zwei gleichen Elementen angesehen werden (was eigene Probleme mit sich bringt). Sollte sich der Bedarf ergeben, ist es immer noch möglich, die Unterstützung für wiederholte Verwendung von Namen später einzuführen.
Es gab Aufrufe, Capture-Muster explizit zu markieren und sie somit als Bindungsziele zu identifizieren. Nach dieser Idee würde ein Capture-Muster als z.B. ?x, $x oder =x geschrieben werden. Das Ziel solcher expliziten Capture-Marker ist es, einen nicht markierten Namen als Wertemuster (siehe unten) zu behandeln. Dies beruht jedoch auf dem Missverständnis, dass Pattern Matching eine Erweiterung von switch-Anweisungen sei und der Schwerpunkt auf schnellem Umschalten basierend auf (ordinalen) Werten liege. Eine solche switch-Anweisung wurde bereits zuvor für Python vorgeschlagen (siehe PEP 275 und PEP 3103). Pattern Matching hingegen baut ein verallgemeinertes Konzept des iterierbaren Entpackens auf. Das Binden von aus einer Datenstruktur extrahierten Werten steht im Kern des Konzepts und ist somit der häufigste Anwendungsfall. Explizite Marker für Capture-Muster würden somit das Ziel der vorgeschlagenen Pattern-Matching-Syntax verraten und einen sekundären Anwendungsfall auf Kosten zusätzlicher syntaktischer Unordnung für Kernfälle vereinfachen.
Es wurde vorgeschlagen, dass Capture-Muster überhaupt nicht benötigt werden, da der entsprechende Effekt durch die Kombination eines AS-Musters mit einem Wildcard-Muster erzielt werden kann (z.B. case _ as x ist äquivalent zu case x). Dies wäre jedoch unangenehm umständlich, insbesondere da wir davon ausgehen, dass Capture-Muster sehr häufig vorkommen werden.
Beispiel für Capture-Muster
def average(*args):
match args:
case [x, y]: # captures the two elements of a sequence
return (x + y) / 2
case [x]: # captures the only element of a sequence
return x
case []:
return 0
case a: # captures the entire sequence
return sum(a) / len(a)
Wildcard-Muster
Das Wildcard-Muster ist ein Sonderfall eines 'Capture'-Musters: Es akzeptiert jeden Wert, bindet ihn aber nicht an eine Variable. Die Idee hinter dieser Regel ist, die wiederholte Verwendung des Wildcards in Mustern zu unterstützen. Während (x, x) ein Fehler ist, ist (_, _) zulässig.
Insbesondere in größeren (Sequenz-)Mustern ist es wichtig, das Muster auf Werte mit tatsächlicher Bedeutung konzentrieren zu lassen und alles andere zu ignorieren. Ohne einen Wildcard wäre es notwendig, eine Reihe von lokalen Variablen zu „erfinden“, die gebunden, aber nie verwendet würden. Selbst wenn man sich an Namenskonventionen hält und z.B. _1, _2, _3 verwendet, um irrelevante Werte zu benennen, führt dies dennoch zu visueller Unordnung und kann die Leistung beeinträchtigen (vergleichen Sie das Sequenzmuster (x, y, *z) mit (_, y, *_), wobei *z den Interpreter zwingt, eine potenziell sehr lange Sequenz zu kopieren, während die zweite Version einfach zu Code wie y = seq[1] kompiliert wird).
Es gab viel Diskussion über die Wahl des Unterstrichs als _ als Wildcard-Muster, d.h. diesen einen Namen nicht bindend zu machen. Der Unterstrich wird jedoch bereits stark als „Ignorierwert“-Markierung beim iterierbaren Entpacken verwendet. Da das Wildcard-Muster _ niemals bindet, stört diese Verwendung des Unterstrichs nicht mit anderen Verwendungen, wie z.B. innerhalb der REPL oder des gettext-Moduls.
Es wurde vorgeschlagen, ... (d.h. das Auslassungszeichen) oder * (Stern) als Wildcard zu verwenden. Beide sehen jedoch so aus, als ob eine beliebige Anzahl von Elementen weggelassen würde
case [a, ..., z]: ...
case [a, *, z]: ...
Jedes Beispiel würde so aussehen, als würde es eine Sequenz von zwei oder mehr Elementen abgleichen, wobei die ersten und letzten Werte erfasst werden. Das mag zwar der ultimative "Wildcard" sein, aber es vermittelt nicht die gewünschte Semantik.
Eine Alternative, die keine beliebige Anzahl von Elementen suggeriert, wäre ?. Dies wird sogar unabhängig vom Pattern Matching in PEP 640 vorgeschlagen. Wir sind jedoch der Meinung, dass die Verwendung von ? als spezielles "Zuweisungsziel" für Python-Benutzer wahrscheinlich verwirrender ist als die Verwendung von _. Es verstößt gegen das (zugegebenermaßen vage) Prinzip von Python, Satzzeichen nur so zu verwenden, wie sie in der englischen Alltagssprache oder in der High School-Mathematik verwendet werden, es sei denn, die Verwendung ist sehr gut in anderen Programmiersprachen etabliert (wie z.B. die Verwendung eines Punkts für den Mitgliedszugriff).
Das Fragezeichen scheitert an beiden Punkten: Seine Verwendung in anderen Programmiersprachen ist ein Sammelsurium von Verwendungen, die nur vage an die Idee einer "Frage" erinnern. Zum Beispiel bedeutet es "beliebiges Zeichen" in Shell Globbing, "vielleicht" in regulären Ausdrücken, "bedingter Ausdruck" in C und vielen C-ähnlichen Sprachen, "Prädikatsfunktion" in Scheme, "Fehlerbehandlung modifizieren" in Rust, "optionales Argument" und "optionales Chaining" in TypeScript (letzteres bedeutet auch für Python vorgeschlagen von PEP 505). Eine noch unbenannte PEP schlägt es zur Markierung optionaler Typen vor, z.B. int?.
Eine weitere häufige Verwendung von ? in Computersystemen ist "Hilfe", z.B. in IPython und Jupyter Notebooks und vielen interaktiven Kommandozeilen-Dienstprogrammen.
Darüber hinaus würde dies Python in eine ziemlich einzigartige Position bringen: Der Unterstrich ist als Wildcard-Muster in jeder Programmiersprache mit Pattern Matching, die wir finden konnten (einschließlich C#, Elixir, Erlang, F#, Grace, Haskell, Mathematica, OCaml, Ruby, Rust, Scala, Swift und Thorn). Bedenkt man, dass viele Python-Benutzer auch mit anderen Programmiersprachen arbeiten, Vorerfahrungen beim Erlernen von Python haben und nach dem Erlernen von Python zu anderen Sprachen wechseln, finden wir, dass solch etablierte Standards in Bezug auf Lesbarkeit und Erlernbarkeit wichtig und relevant sind. Unserer Meinung nach sind Bedenken, dass dieser Wildcard bedeutet, dass ein regulärer Name eine Sonderbehandlung erhält, nicht stark genug, um eine Syntax einzuführen, die Python speziell machen würde.
Else-Blöcke. Ein Case-Block ohne Guard, dessen Muster ein einzelner Wildcard ist (d.h. case _:), akzeptiert jedes Subjekt, ohne es an eine Variable zu binden oder eine andere Operation durchzuführen. Er ist somit semantisch äquivalent zu else:, falls dies unterstützt würde. Das Hinzufügen eines solchen Else-Blocks zur Match-Statement-Syntax würde jedoch die Notwendigkeit des Wildcard-Musters in anderen Kontexten nicht beseitigen. Ein weiteres Argument dagegen ist, dass es zwei plausible Einrückungsebenen für einen Else-Block gäbe: ausgerichtet an case oder ausgerichtet an match. Die Autoren fanden es ziemlich umstritten, welche Einrückungsebene bevorzugt werden sollte.
Beispiel für das Wildcard-Muster
def is_closed(sequence):
match sequence:
case [_]: # any sequence with a single element
return True
case [start, *_, end]: # a sequence with at least two elements
return start == end
case _: # anything
return False
Wertmuster
Es ist guter Programmierstil, benannte Konstanten für parametrische Werte zu verwenden oder die Bedeutung bestimmter Werte zu verdeutlichen. Offensichtlich wäre es vorzuziehen, z.B. case (HttpStatus.OK, body): anstelle von case (200, body): zu schreiben. Das Hauptproblem, das hier auftritt, ist, wie Capture-Muster (Variablenbindungen) von Wertemustern unterschieden werden können. Die allgemeine Diskussion zu diesem Thema hat eine Fülle von Optionen hervorgebracht, die wir hier nicht alle vollständig aufzählen können.
Streng genommen sind Wertemuster nicht wirklich notwendig, sondern könnten mithilfe von Guards implementiert werden, d.h. case (status, body) if status == HttpStatus.OK:. Dennoch ist die Bequemlichkeit von Wertemustern unbestritten und offensichtlich.
Die Beobachtung, dass Konstanten oft in Großbuchstaben geschrieben oder in Aufzählung-ähnlichen Namespaces gesammelt werden, legt mögliche Regeln zur syntaktischen Unterscheidung von Konstanten nahe. Die Idee, Groß- vs. Kleinschreibung als Marker zu verwenden, stieß jedoch auf Skepsis, da es in Kern-Python keine ähnliche Präzedenz gibt (obwohl sie in anderen Sprachen üblich ist). Wir haben daher nur die Regel übernommen, dass jeder gepunktete Name (d.h. Attributzugriff) als Wertemuster interpretiert wird, wie z.B. HttpStatus.OK oben. Dies schließt insbesondere lokale Variablen und globale Variablen des aktuellen Moduls von der Verwendung als Konstanten aus.
Eine vorgeschlagene Regel, einen führenden Punkt (z.B. .CONSTANT) für diesen Zweck zu verwenden, wurde kritisiert, da man der Meinung war, dass der Punkt kein ausreichend sichtbarer Marker für diesen Zweck sei. Teilweise inspiriert von Formen aus anderen Programmiersprachen wurden eine Reihe verschiedener Marker/Sigils vorgeschlagen (wie z.B. ^CONSTANT, $CONSTANT, ==CONSTANT, CONSTANT?, oder das in Backticks eingeschlossene Wort), obwohl es keine offensichtliche oder natürliche Wahl gab. Der aktuelle Vorschlag lässt daher die Diskussion und mögliche Einführung eines solchen 'Konstanten'-Markers für eine zukünftige PEP offen.
Die Unterscheidung der Semantik von Namen danach, ob es sich um eine globale Variable handelt (d.h. der Compiler würde globale Variablen als Konstanten und nicht als Capture-Muster behandeln), führt zu verschiedenen Problemen. Die Hinzufügung oder Änderung einer globalen Variable im Modul könnte unbeabsichtigte Nebenwirkungen auf Muster haben. Darüber hinaus könnte Pattern Matching nicht direkt innerhalb des Geltungsbereichs eines Moduls verwendet werden, da alle Variablen global wären, was Capture-Muster unmöglich machen würde.
Beispiel für das Wertemuster
def handle_reply(reply):
match reply:
case (HttpStatus.OK, MimeType.TEXT, body):
process_text(body)
case (HttpStatus.OK, MimeType.APPL_ZIP, body):
text = deflate(body)
process_text(text)
case (HttpStatus.MOVED_PERMANENTLY, new_URI):
resend_request(new_URI)
case (HttpStatus.NOT_FOUND):
raise ResourceNotFound()
Gruppenmuster
Die explizite Angabe der Gruppierung durch die Benutzer ist besonders hilfreich bei OR-Mustern.
Sequenzmuster
Sequenzmuster folgen so genau wie möglich der bereits etablierten Syntax und Semantik des iterierbaren Entpackens. Natürlich treten Teilmuster an die Stelle von Zuweisungszielen (Variablen, Attribute und Indizes). Darüber hinaus passt das Sequenzmuster nur zu einer sorgfältig ausgewählten Menge möglicher Subjekte, während iterierbares Entpacken auf jedes iterierbare Objekt angewendet werden kann.
- Wie beim iterierbaren Entpacken unterscheiden wir nicht zwischen 'Tupel'- und 'Listen'-Notation.
[a, b, c],(a, b, c)unda, b, csind alle äquivalent. Obwohl dies bedeutet, dass wir eine redundante Notation haben und die spezifische Prüfung auf Listen oder Tupel mehr Aufwand erfordert (z.B.case list([a, b, c])), ahmen wir das iterierbare Entpacken so weit wie möglich nach. - Ein Sternmuster erfasst eine Teilsequenz beliebiger Länge, wiederum nach dem Vorbild des iterierbaren Entpackens. Nur ein sternförmiges Element darf in jedem Sequenzmuster vorhanden sein. Theoretisch könnten Muster wie
(*_, 3, *_)so verstanden werden, dass sie jede Sequenz ausdrücken, die den Wert3enthält. In der Praxis würde dies jedoch nur für einen sehr begrenzten Satz von Anwendungsfällen funktionieren und sonst zu ineffizientem Backtracking oder sogar Mehrdeutigkeiten führen. - Das Sequenzmuster durchläuft einen iterierbaren Subjekt nicht. Alle Elemente werden über Indizierung und Slicing abgerufen, und das Subjekt muss eine Instanz von
collections.abc.Sequencesein. Dazu gehören natürlich Listen und Tupel, aber ausgeschlossen sind z.B. Mengen und Wörterbücher. Obwohl es Strings und Bytes einschließen würde, machen wir eine Ausnahme für diese (siehe unten).
Ein Sequenzmuster kann nicht einfach durch ein beliebiges iterierbares Objekt iterieren. Der Verbrauch von Elementen aus der Iteration müsste rückgängig gemacht werden, wenn das Gesamtmuster fehlschlägt, was nicht machbar ist.
Um Sequenzen zu identifizieren, können wir uns nicht allein auf len() und Indizierung und Slicing verlassen, da Sequenzen in dieser Hinsicht Protokolle mit Mappings (z.B. dict) teilen. Es wäre überraschend, wenn ein Sequenzmuster auch Wörterbücher oder andere Objekte abgleichen würde, die das Mapping-Protokoll implementieren (d.h. __getitem__). Der Interpreter führt daher eine Instanzprüfung durch, um sicherzustellen, dass das betreffende Subjekt wirklich eine Sequenz (bekannten Typs) ist. (Als Optimierung des häufigsten Falls kann die Instanzprüfung übersprungen werden, wenn das Subjekt genau eine Liste oder ein Tupel ist.)
String- und Bytes-Objekte haben eine doppelte Natur: Sie sind sowohl eigenständige 'atomare' Objekte als auch Sequenzen (mit einer stark rekursiven Natur, da ein String eine Sequenz von Strings ist). Das typische Verhalten und die Anwendungsfälle für Strings und Bytes unterscheiden sich genug von denen von Tupeln und Listen, um eine klare Unterscheidung zu rechtfertigen. Es ist tatsächlich oft unintuitiv und unbeabsichtigt, dass Strings als Sequenzen gelten, wie regelmäßige Fragen und Beschwerden belegen. Strings und Bytes werden daher nicht von einem Sequenzmuster abgeglichen, was das Sequenzmuster auf ein sehr spezifisches Verständnis von 'Sequenz' beschränkt. Der eingebaute Typ bytearray, der eine veränderliche Version von bytes ist, verdient ebenfalls eine Ausnahme; wir beabsichtigen jedoch nicht, alle anderen Typen aufzuzählen, die zur Darstellung von Bytes verwendet werden können (z.B. einige, aber nicht alle Instanzen von memoryview und array.array).
Abgleichmuster
Wörterbücher oder Mappings im Allgemeinen sind eine der wichtigsten und am weitesten verbreiteten Datenstrukturen in Python. Im Gegensatz zu Sequenzen sind Mappings für den schnellen direkten Zugriff auf beliebige Elemente konzipiert, die durch einen Schlüssel identifiziert werden. In den meisten Fällen wird ein Element aus einem Wörterbuch anhand eines bekannten Schlüssels abgerufen, ohne Rücksicht auf eine Reihenfolge oder andere Schlüssel-Wert-Paare, die im selben Wörterbuch gespeichert sind. Besonders häufig sind String-Schlüssel.
Das Mapping-Muster spiegelt den gängigen Gebrauch der Wörterbuchsuche wider: Es erlaubt dem Benutzer, Werte aus einem Mapping anhand von konstanten/bekannten Schlüsseln zu extrahieren und die Werte mit gegebenen Teilmustern abzugleichen. Zusätzliche Schlüssel im Subjekt werden ignoriert, auch wenn **rest nicht vorhanden ist. Dies unterscheidet sich von Sequenzmustern, bei denen zusätzliche Elemente zum Fehlschlagen eines Abgleichs führen. Mappings unterscheiden sich jedoch tatsächlich von Sequenzen: Sie haben ein natürliches strukturelles Subtyping-Verhalten, d.h. das Übergeben eines Wörterbuchs mit zusätzlichen Schlüsseln funktioniert wahrscheinlich einfach. Sollte es notwendig sein, eine Obergrenze für das Mapping festzulegen und sicherzustellen, dass keine zusätzlichen Schlüssel vorhanden sind, kann das übliche Doppelsternmuster **rest verwendet werden. Der Sonderfall **_ mit einem Wildcard wird jedoch nicht unterstützt, da er keine Wirkung hätte, aber zu einem falschen Verständnis der Semantik des Mapping-Musters führen könnte.
Um übermäßig teure Abgleichalgorithmen zu vermeiden, müssen Schlüssel Literale oder Wertemuster sein.
Es gibt einen subtilen Grund, get(key, default) anstelle von __getitem__(key) gefolgt von einer Prüfung auf AttributeError zu verwenden: Wenn das Subjekt zufällig ein defaultdict ist, würde das Aufrufen von __getitem__ für einen nicht vorhandenen Schlüssel den Schlüssel hinzufügen. Die Verwendung von get() vermeidet diesen unerwarteten Nebeneffekt.
Beispiel für das Mapping-Muster
def change_red_to_blue(json_obj):
match json_obj:
case { 'color': ('red' | '#FF0000') }:
json_obj['color'] = 'blue'
case { 'children': children }:
for child in children:
change_red_to_blue(child)
Klassenmuster
Klassenmuster erfüllen zwei Zwecke: die Überprüfung, ob ein gegebenes Subjekt tatsächlich eine Instanz einer bestimmten Klasse ist, und die Extraktion von Daten aus spezifischen Attributen des Subjekts. Anekdotische Beweise zeigten, dass isinstance() eine der am häufigsten verwendeten Funktionen in Python in Bezug auf statische Vorkommen in Programmen ist. Solche Instanzprüfungen gehen typischerweise einer anschließenden Zugriff auf Informationen im Objekt oder einer möglichen Manipulation davon voraus. Ein typisches Muster könnte in der Art von
def traverse_tree(node):
if isinstance(node, Node):
traverse_tree(node.left)
traverse_tree(node.right)
elif isinstance(node, Leaf):
print(node.value)
In vielen Fällen sind Klassenmuster verschachtelt, wie im Beispiel in der Motivation
if (isinstance(node, BinOp) and node.op == "+"
and isinstance(node.right, BinOp) and node.right.op == "*"):
a, b, c = node.left, node.right.left, node.right.right
# Handle a + b*c
Das Klassenmuster ermöglicht es Ihnen, sowohl eine Instanzprüfung als auch relevante Attribute (mit möglichen weiteren Einschränkungen) prägnant anzugeben. Es ist daher sehr verlockend, z.B. case Node(left, right): im ersten Fall und case Leaf(value): im zweiten Fall zu schreiben. Obwohl dies für Sprachen mit strengen algebraischen Datentypen gut funktioniert, ist es mit der Struktur von Python-Objekten problematisch.
Beim Umgang mit allgemeinen Python-Objekten sehen wir uns einer potenziell sehr großen Anzahl ungeordneter Attribute gegenüber: Eine Instanz von Node enthält eine große Anzahl von Attributen (von denen die meisten 'spezielle Methoden' wie __repr__ sind). Darüber hinaus kann der Interpreter die Reihenfolge der Attribute nicht zuverlässig ableiten. Für ein Objekt, das z.B. einen Kreis darstellt, gibt es keine inhärent offensichtliche Reihenfolge der Attribute x, y und radius.
Wir sehen zwei Möglichkeiten, dieses Problem anzugehen: Entweder die Attribute von Interesse explizit benennen oder eine zusätzliche Zuordnung bereitstellen, die dem Interpreter mitteilt, welche Attribute extrahiert und in welcher Reihenfolge. Beide Ansätze werden unterstützt. Darüber hinaus lässt die explizite Benennung der Attribute von Interesse die weitere Spezifikation der erforderlichen Struktur eines Objekts zu; wenn einem Objekt ein im Muster angegebenes Attribut fehlt, schlägt der Abgleich fehl.
- Explizit benannte Attribute übernehmen die Syntax von benannten Argumenten. Wenn ein Objekt der Klasse
Nodewie oben zwei Attributeleftundrighthat, extrahiert das MusterNode(left=x, right=y)die Werte beider Attribute und weist siexbzw.yzu. Der Datenfluss von links nach rechts erscheint ungewöhnlich, ist aber im Einklang mit Mapping-Mustern und hat Präzedenzfälle wie Zuweisungen überasin with- oder import-Anweisungen (und tatsächlich AS-Mustern).Die explizite Benennung der betreffenden Attribute wird hauptsächlich für komplexere Fälle verwendet, bei denen die Positionsform (unten) nicht ausreicht.
- Das Klassenfeld
__match_args__gibt eine Anzahl von Attributen zusammen mit ihrer Reihenfolge an, wodurch Klassenmuster auf positionelle Teilmuster zurückgreifen können, ohne die betreffenden Attribute explizit benennen zu müssen. Dies ist besonders praktisch für kleinere Objekte oder Instanzen von Datenklassen, bei denen die Attribute von Interesse eher offensichtlich sind und oft eine wohldefinierte Reihenfolge haben. In gewisser Weise ähnelt__match_args__der Deklaration von formalen Parametern, die es ermöglicht, Funktionen mit positionellen Argumenten aufzurufen, anstatt alle Parameter zu benennen.Dies ist ein Klassenattribut, da es auf der Klasse nachgeschlagen werden muss, die im Klassenmuster genannt wird, nicht auf der Instanz des Subjekts.
Die Syntax von Klassenmustern basiert auf der Idee, dass die Dekonstruktion die Syntax der Konstruktion widerspiegelt. Dies ist bereits in praktisch jedem Python-Konstrukt der Fall, sei es bei Zuweisungszielen, Funktionsdefinitionen oder iterierbarem Entpacken. In all diesen Fällen finden wir, dass die Syntax zum Senden und die zum Empfangen von 'Daten' praktisch identisch sind.
- Zuweisungsziele wie Variablen, Attribute und Indizes:
foo.bar[2] = foo.bar[3]; - Funktionsdefinitionen: Eine Funktion, die mit
def foo(x, y, z=6)definiert wird, wird z.B. aufgerufen alsfoo(123, y=45), wobei die tatsächlichen Argumente an der Aufrufstelle gegen die formalen Parameter an der Definitionsstelle abgeglichen werden; - Iterierbares Entpacken:
a, b = b, aoder[a, b] = [b, a]oder(a, b) = (b, a), um nur einige äquivalente Möglichkeiten zu nennen.
Die Verwendung derselben Syntax für Lesen und Schreiben, l- und r-Werte, oder Konstruktion und Dekonstruktion ist weitgehend anerkannt für ihre Vorteile beim Denken über Daten, ihren Fluss und ihre Manipulation. Dies erstreckt sich gleichermaßen auf die explizite Konstruktion von Instanzen, bei denen Klassenmuster C(p, q) bewusst die Syntax der Erstellung von Instanzen widerspiegeln.
Der Sonderfall für die eingebauten Klassen bool, bytearray usw. (wo z.B. str(x) den Subjektwert in x erfasst) kann durch eine benutzerdefinierte Klasse wie folgt emuliert werden
class MyClass:
__match_args__ = ["__myself__"]
__myself__ = property(lambda self: self)
Typ-Annotationen für Muster-Variablen. Der Vorschlag war, Muster mit Typ-Annotationen 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("Three ints", a, b, c)
...
Diese Idee hat viele Probleme. Zum einen kann der Doppelpunkt nur innerhalb von Klammern oder runden Klammern verwendet werden, da sonst die Syntax mehrdeutig wird. Und weil Python isinstance()-Prüfungen auf generischen Typen nicht zulässt, funktionieren Typ-Annotationen, die Generics enthalten, nicht wie erwartet.
Geschichte und Kontext
Pattern Matching entstand in den späten 1970er Jahren in Form von Tupel-Entpackung und als Mittel zur Behandlung rekursiver Datenstrukturen wie verknüpfte Listen oder Bäume (objektorientierte Sprachen verwenden normalerweise das Visitor-Pattern zur Behandlung rekursiver Datenstrukturen). Die frühen Befürworter von Pattern Matching organisierten strukturierte Daten in 'getaggten Tupeln' anstelle von struct wie in C oder den später eingeführten Objekten. Ein Knoten in einem binären Baum wäre beispielsweise ein Tupel mit zwei Elementen für die linken und rechten Zweige bzw. ein Node-Tag, geschrieben als Node(left, right). In Python würden wir wahrscheinlich das Tag innerhalb des Tupels als ('Node', left, right) einfügen oder eine Datenklasse Node definieren, um denselben Effekt zu erzielen.
Mit moderner Syntax würde eine Tiefensuche im Baum dann wie folgt geschrieben werden
def traverse(node):
match node:
case Node(left, right):
traverse(left)
traverse(right)
case Leaf(value):
handle(value)
Die Vorstellung, rekursive Datenstrukturen mit Pattern Matching zu handhaben, führte sofort zur Idee, allgemeinere rekursive 'Muster' (d.h. Rekursion über rekursive Datenstrukturen hinaus) mit Pattern Matching zu handhaben. Pattern Matching würde somit auch verwendet werden, um rekursive Funktionen zu definieren wie
def fib(arg):
match arg:
case 0:
return 1
case 1:
return 1
case n:
return fib(n-1) + fib(n-2)
Als Pattern Matching wiederholt in neue und aufkommende Programmiersprachen integriert wurde, entwickelte sich seine Syntax leicht weiter und erweiterte sich. Die beiden ersten Fälle im fib-Beispiel oben könnten prägnanter als case 0 | 1: mit | geschrieben werden, das alternative Muster bezeichnet. Darüber hinaus wurde der Unterstrich _ weithin als Wildcard übernommen, als Füllzeichen, wo weder die Struktur noch der Wert von Teilen eines Musters von Substanz waren. Da der Unterstrich bereits in Python's iterierbarem Entpacken häufig in gleicher Funktion verwendet wird (z.B. _, _, third, _* = something), haben wir diese universellen Standards beibehalten.
Es ist erwähnenswert, dass das Konzept des Pattern Matching immer eng mit dem Konzept von Funktionen verbunden war. Die verschiedenen Case-Klauseln wurden immer als etwas wie semi-unabhängige Funktionen betrachtet, bei denen Muster-Variablen die Rolle von Parametern übernehmen. Dies wird am deutlichsten, wenn Pattern Matching als überladene Funktion geschrieben wird, in der Art von (Standard ML)
fun fib 0 = 1
| fib 1 = 1
| fib n = fib (n-1) + fib (n-2)
Auch wenn eine solche strikte Trennung von Fall-Klauseln in unabhängige Funktionen in Python nicht gilt, stellen wir fest, dass Muster viele syntaktische Regeln mit Parametern teilen, wie z. B. die Bindung von Argumenten nur an nicht qualifizierte Namen oder dass Variablen-/Parameternamen für ein bestimmtes Muster/eine bestimmte Funktion nicht wiederholt werden dürfen.
Mit seinem Schwerpunkt auf Abstraktion und Kapselung stellte die objektorientierte Programmierung eine ernsthafte Herausforderung für die Mustererkennung dar. Kurz gesagt: In der objektorientierten Programmierung können wir Objekte nicht mehr als getaggte Tupel betrachten. Die an den Konstruktor übergebenen Argumente geben nicht notwendigerweise die Attribute oder Felder der Objekte an. Darüber hinaus gibt es keine feste Reihenfolge der Felder eines Objekts mehr, und einige Felder könnten privat und somit unzugänglich sein. Und darüber hinaus könnte das gegebene Objekt tatsächlich eine Instanz einer Unterklasse mit leicht unterschiedlicher Struktur sein.
Um dieser Herausforderung zu begegnen, wurden Muster zunehmend unabhängig von den ursprünglichen Tupelkonstruktoren. In einem Muster wie Node(left, right) ist Node nicht mehr nur ein passives Tag, sondern vielmehr eine Funktion, die für jedes gegebene Objekt aktiv überprüfen kann, ob es die richtige Struktur hat und ein left- und ein right-Feld extrahieren kann. Mit anderen Worten: Das Node-Tag wird zu einer Funktion, die ein Objekt in ein Tupel umwandelt oder einen Fehlerindikator zurückgibt, wenn dies nicht möglich ist.
In Python verwenden wir einfach isinstance() zusammen mit dem Feld __match_args__ einer Klasse, um zu prüfen, ob ein Objekt die richtige Struktur hat, und wandeln dann einige seiner Attribute in ein Tupel um. Für das Node-Beispiel oben hätten wir beispielsweise __match_args__ = ('left', 'right'), um anzuzeigen, dass diese beiden Attribute extrahiert werden sollen, um das Tupel zu bilden. Das heißt, case Node(x, y) würde zuerst prüfen, ob ein gegebenes Objekt eine Instanz von Node ist, und dann left x und right y zuweisen.
Als Tribut an Pythons dynamische Natur mit „Duck-Typing“ haben wir jedoch auch einen direkteren Weg hinzugefügt, um die Anwesenheit oder Einschränkungen spezifischer Attribute anzugeben. Anstelle von Node(x, y) könnte man auch object(left=x, right=y) schreiben, wodurch die isinstance()-Prüfung effektiv entfällt und somit jedes Objekt mit left- und right-Attributen unterstützt wird. Oder man könnte diese Ideen kombinieren, um Node(right=y) zu schreiben, um eine Instanz von Node zu verlangen, aber nur den Wert des right-Attributs zu extrahieren.
Abwärtskompatibilität
Durch die Verwendung von „Soft Keywords“ und dem neuen PEG-Parser (PEP 617) bleibt der Vorschlag vollständig abwärtskompatibel. 3rd-Party-Tools, die einen LL(1)-Parser zur Analyse von Python-Quellcode verwenden, müssen jedoch möglicherweise auf eine Parser-Technologie umsteigen, um dieselben Funktionen unterstützen zu können.
Sicherheitsimplikationen
Wir erwarten keine Sicherheitsauswirkungen von diesem Sprachfeature.
Referenzimplementierung
Eine feature-complete CPython-Implementierung ist auf GitHub verfügbar.
Ein interaktiver Playground, der auf der obigen Implementierung basiert, wurde mit Binder [2] und Jupyter [3] erstellt.
Referenzen
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0635.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT