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

Python Enhancement Proposals

PEP 3103 – Eine Switch/Case-Anweisung

Autor:
Guido van Rossum <guido at python.org>
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
25. Juni 2006
Python-Version:
3.0
Post-History:
26. Juni 2006

Inhaltsverzeichnis

Ablehnungsbescheid

Eine schnelle Umfrage während meiner Keynote-Präsentation auf der PyCon 2007 zeigt, dass dieser Vorschlag keine allgemeine Unterstützung hat. Ich lehne ihn daher ab.

Zusammenfassung

Auf python-dev gab es kürzlich eine Flut von Diskussionen über die Einführung einer Switch-Anweisung. In diesem PEP versuche ich, meine eigenen Präferenzen aus dem Smorgasbord von Vorschlägen zu extrahieren, Alternativen zu diskutieren und meine Entscheidungen zu erläutern, wo ich kann. Ich werde auch angeben, wie stark ich zu den Alternativen stehe, die ich bespreche.

Dieser PEP sollte als Alternative zu PEP 275 betrachtet werden. Meine Ansichten unterscheiden sich etwas von denen des Autors dieses PEP, aber ich bin dankbar für die Arbeit, die in diesem PEP geleistet wurde.

Dieser PEP führt kanonische Namen für die vielen Varianten ein, die für verschiedene Aspekte der Syntax und Semantik diskutiert wurden, wie z. B. "Alternative 1", "Schule II", "Option 3" und so weiter. Hoffentlich werden diese Namen die Diskussion erleichtern.

Begründung

Ein gängiges Programmieridiom ist es, einen Ausdruck zu betrachten und je nach seinem Wert unterschiedliche Dinge zu tun. Dies geschieht normalerweise mit einer Kette von if/elif-Tests; ich werde diese Form als "if/elif-Kette" bezeichnen. Es gibt zwei Hauptmotivationen, eine neue Syntax für dieses Idiom einführen zu wollen

  • Es ist repetitiv: Die Variable und der Testoperator, normalerweise '==' oder 'in', werden in jedem if/elif-Zweig wiederholt.
  • Es ist ineffizient: Wenn ein Ausdruck mit dem letzten Testwert übereinstimmt (oder gar keinem Testwert), wird er mit jedem der vorhergehenden Testwerte verglichen.

Beide Beschwerden sind relativ geringfügig; es gibt nicht viel Lesbarkeit oder Leistung zu gewinnen, indem man dies anders schreibt. Dennoch gibt es in vielen Sprachen eine Art Switch-Anweisung, und es ist nicht unangemessen zu erwarten, dass ihre Hinzufügung zu Python es uns ermöglicht, bestimmten Code sauberer und effizienter als zuvor zu schreiben.

Es gibt Dispatch-Formen, die für die vorgeschlagene Switch-Anweisung nicht geeignet sind; zum Beispiel, wenn die Anzahl der Fälle nicht statisch bekannt ist oder wenn es wünschenswert ist, den Code für verschiedene Fälle in verschiedenen Klassen oder Dateien zu platzieren.

Grundlegende Syntax

Ich betrachte hier mehrere Varianten der Syntax, die erstmals in PEP 275 vorgeschlagen wurden. Es gibt viele andere Möglichkeiten, aber ich sehe nicht, dass sie etwas hinzufügen.

Ich wurde kürzlich zu Alternative 1 bekehrt.

Ich sollte anmerken, dass alle Alternativen hier die "implizite break"-Eigenschaft haben: Am Ende der Suite für einen bestimmten Fall springt der Kontrollfluss zum Ende der gesamten Switch-Anweisung. Es gibt keine Möglichkeit, die Kontrolle von einem Fall zum anderen zu übertragen. Dies steht im Gegensatz zu C, wo eine explizite 'break'-Anweisung erforderlich ist, um ein Durchfallen zum nächsten Fall zu verhindern.

In allen Alternativen ist die else-Suite optional. Es ist Pythonischer, hier 'else' zu verwenden, anstatt ein neues reserviertes Wort wie 'default' wie in C einzuführen.

Die Semantik wird im nächsten Hauptabschnitt besprochen.

Alternative 1

Dies ist die bevorzugte Form in PEP 275

switch EXPR:
    case EXPR:
        SUITE
    case EXPR:
        SUITE
    ...
    else:
        SUITE

Der Hauptnachteil ist, dass die Suiten, in denen die eigentliche Aktion stattfindet, zwei Ebenen tief eingerückt sind; dies kann durch Einrücken der Fälle um "einen halben Level" behoben werden (z. B. 2 Leerzeichen, wenn die allgemeine Einrückungsebene 4 beträgt).

Alternative 2

Dies ist die bevorzugte Form von Fredrik Lundh; sie unterscheidet sich dadurch, dass die Fälle nicht eingerückt werden

switch EXPR:
case EXPR:
    SUITE
case EXPR:
    SUITE
....
else:
    SUITE

Einige Gründe, diese nicht zu wählen, sind erwartete Schwierigkeiten für Auto-Einrückungseditoren, Falten-Editoren und dergleichen; und verwirrte Benutzer. Derzeit gibt es in Python keine Situationen, in denen eine Zeile, die mit einem Doppelpunkt endet, von einer nicht eingerückten Zeile gefolgt wird.

Alternative 3

Dies ist dasselbe wie Alternative 2, lässt jedoch den Doppelpunkt nach dem Switch weg

switch EXPR
case EXPR:
    SUITE
case EXPR:
    SUITE
....
else:
    SUITE

Die Hoffnung dieser Alternative ist, dass sie die Auto-Einrückungslogik des durchschnittlichen Python-bewussten Texteditors weniger stört. Aber sie sieht für mich seltsam aus.

Alternative 4

Dies lässt das Schlüsselwort 'case' weg, da es redundant ist

switch EXPR:
    EXPR:
        SUITE
    EXPR:
        SUITE
    ...
    else:
        SUITE

Leider sind wir nun gezwungen, die Case-Ausdrücke einzurücken, da ansonsten (zumindest in Abwesenheit des Schlüsselworts 'else') der Parser Schwierigkeiten hätte zu unterscheiden, ob ein nicht eingerückter Case-Ausdruck (der die Switch-Anweisung fortsetzt) oder eine unabhängige Anweisung ist, die wie ein Ausdruck beginnt (z. B. eine Zuweisung oder ein Prozeduraufruf). Der Parser ist nicht intelligent genug, um zurückzuspulen, sobald er den Doppelpunkt sieht. Dies ist meine am wenigsten bevorzugte Alternative.

Erweiterte Syntax

Es gibt eine zusätzliche Überlegung, die syntaktisch behandelt werden muss. Oft müssen zwei oder mehr Werte gleich behandelt werden. In C geschieht dies, indem mehrere Case-Labels ohne Code dazwischen geschrieben werden. Die "Fall-Through"-Semantik bedeutet dann, dass diese alle vom selben Code behandelt werden. Da die Python-Switch keine Fall-Through-Semantik haben wird (die noch keinen Befürworter gefunden hat), brauchen wir eine andere Lösung. Hier sind einige Alternativen.

Alternative A

Verwenden Sie

case EXPR:

um auf einen einzelnen Ausdruck zu prüfen; verwenden Sie

case EXPR, EXPR, ...:

um auf mehrere Ausdrücke zu prüfen. Das wird so interpretiert, dass, wenn EXPR ein geklammerter Tupel oder ein anderer Ausdruck ist, dessen Wert ein Tupel ist, der Switch-Ausdruck gleich diesem Tupel sein muss, nicht einem seiner Elemente. Das bedeutet, dass wir keine Variable verwenden können, um mehrere Fälle anzuzeigen. Während dies auch in C's Switch-Anweisung der Fall ist, ist es in Python relativ häufig vorkommend (siehe zum Beispiel sre_compile.py).

Alternative B

Verwenden Sie

case EXPR:

um auf einen einzelnen Ausdruck zu prüfen; verwenden Sie

case in EXPR_LIST:

um auf mehrere Ausdrücke zu prüfen. Wenn EXPR_LIST ein einzelner Ausdruck ist, zwingt das 'in' seine Interpretation als ein Iterable (oder etwas, das __contains__ unterstützt, in einer Minderheiten-Semantik-Alternative). Wenn es mehrere Ausdrücke sind, wird jeder davon für eine Übereinstimmung betrachtet.

Alternative C

Verwenden Sie

case EXPR:

um auf einen einzelnen Ausdruck zu prüfen; verwenden Sie

case EXPR, EXPR, ...:

um auf mehrere Ausdrücke zu prüfen (wie in Alternative A); und verwenden Sie

case *EXPR:

um auf die Elemente eines Ausdrucks zu prüfen, dessen Wert ein Iterable ist. Die beiden letzteren Fälle können kombiniert werden, so dass die tatsächliche Syntax eher so aussieht

case [*]EXPR, [*]EXPR, ...:

Die Notation mit dem * ähnelt der Verwendung des Präfixes *, das bereits für Parameterlisten variabler Länge und für die Übergabe berechneter Argumentlisten verwendet wird und oft für die Wertentpackung vorgeschlagen wird (z. B. a, b, *c = X als Alternative zu X[:2], X[2:]).

Alternative D

Dies ist eine Mischung aus den Alternativen B und C; die Syntax ist wie Alternative B, verwendet aber anstelle des Schlüsselworts 'in' ein '*'. Dies ist begrenzter, erlaubt aber immer noch die gleiche Flexibilität. Es verwendet

case EXPR:

um auf einen einzelnen Ausdruck zu prüfen und

case *EXPR:

um auf die Elemente eines Iterables zu prüfen. Wenn man mehrere Übereinstimmungen in einem Fall angeben möchte, kann man dies so schreiben

case *(EXPR, EXPR, ...):

oder vielleicht so (obwohl es etwas seltsam ist, da die relative Priorität von '*' und ',' anders ist als anderswo)

case * EXPR, EXPR, ...:

Diskussion

Die Alternativen B, C und D sind motiviert durch den Wunsch, mehrere Fälle mit der gleichen Behandlung durch eine Variable zu spezifizieren, die eine Menge (typischerweise ein Tupel) repräsentiert, anstatt sie buchstäblich aufzulisten. Die Motivation dafür ist normalerweise, dass es schade ist, wenn man mehrere Switches über dieselben Fälle hat, alle Alternativen jedes Mal aufzählen zu müssen. Eine zusätzliche Motivation ist die Möglichkeit, *Bereiche* einfach und effizient abzugleichen, ähnlich der Pascalschen "1..1000:"-Notation. Gleichzeitig wollen wir die Art von Fehlern verhindern, die bei der Ausnahmebehandlung üblich sind (und die in Python 3000 durch Änderung der Syntax der except-Klausel behoben werden): Schreiben von "case 1, 2:" anstelle von "case (1, 2):" oder umgekehrt.

Man könnte argumentieren, dass der Bedarf nicht ausreichend für die zusätzliche Komplexität ist; C hat keine Möglichkeit, Bereiche auszudrücken, und es wird heutzutage viel mehr verwendet als Pascal. Außerdem könnten bei einer Dispatch-Methode, die auf Dict-Lookup basiert, große Bereiche ineffizient sein (betrachten Sie range(1, sys.maxint)).

Alles in allem sind meine Präferenzen (von der am meisten zur am wenigsten bevorzugten) B, A, D', C, wobei D' D ohne die dritte Möglichkeit ist.

Semantik

Es gibt verschiedene Punkte zu überprüfen, bevor wir die richtige Semantik wählen können.

If/Elif-Kette vs. Dict-basierter Dispatch

Es gibt mehrere Hauptdenkschulen bezüglich der Semantik der Switch-Anweisung

  • Schule I möchte die Switch-Anweisung im Sinne einer äquivalenten if/elif-Kette definieren (möglicherweise mit einigen Optimierungen).
  • Schule II zieht es vor, sie als Dispatch auf ein vorab berechnetes Dict zu betrachten. Es gibt verschiedene Entscheidungen, wann die Vorberechnung stattfindet.
  • Es gibt auch Schule III, die mit Schule I darin übereinstimmt, dass die Definition einer Switch-Anweisung im Sinne einer äquivalenten if/elif-Kette erfolgen sollte, aber dem Optimierungslager zugesteht, dass alle beteiligten Ausdrücke hashbar sein müssen.

Wir müssen Schule I weiter in Schule Ia und Schule Ib unterteilen

  • Schule Ia hat eine einfache Position: Eine Switch-Anweisung wird in eine äquivalente if/elif-Kette übersetzt, und das ist alles. Sie sollte nicht mit Optimierung in Verbindung gebracht werden. Das ist auch mein Hauptargument gegen diese Schule: Ohne jeglichen Hinweis auf Optimierung ist die Switch-Anweisung nicht attraktiv genug, um neue Syntax zu rechtfertigen.
  • Schule Ib hat eine komplexere Position: Sie stimmt mit Schule II überein, dass Optimierung wichtig ist, und ist bereit, dem Compiler gewisse Freiheiten einzuräumen, um dies zu ermöglichen. (Zum Beispiel PEP 275 Lösung 1.) Insbesondere können hash() des Switches und der Case-Ausdrücke möglicherweise nicht aufgerufen werden (daher sollten sie nebenwirkungsfrei sein); und die Case-Ausdrücke dürfen nicht jedes Mal ausgewertet werden, wie es das if/elif-Kettenverhalten erwartet, daher sollten auch die Case-Ausdrücke nebenwirkungsfrei sein. Mein Einwand dagegen (weiter unten ausgeführt) ist, dass, wenn entweder hash() oder die Case-Ausdrücke nicht nebenwirkungsfrei sind, optimierter und nicht optimierter Code unterschiedlich verhalten kann.

Schule II entstand aus der Erkenntnis, dass die Optimierung häufig vorkommender Fälle nicht so einfach ist und dass es besser ist, sich dem direkt zu stellen. Dies wird weiter unten deutlich.

Die Unterschiede zwischen Schule I (hauptsächlich Schule Ib) und Schule II sind dreifach

  • Bei der Optimierung mit einem Dispatch-Dict muss, wenn entweder der Switch-Ausdruck oder die Case-Ausdrücke unhashbar sind (in diesem Fall löst hash() eine Ausnahme aus), Schule Ib das hash()-Fehlerschlagen abfangen und auf eine if/elif-Kette zurückgreifen. Schule II lässt einfach die Ausnahme geschehen. Das Problem beim Abfangen einer Ausnahme in hash() wie von Schule Ib gefordert, ist, dass dies einen echten Fehler verbergen kann. Ein möglicher Ausweg ist, nur dann ein Dispatch-Dict zu verwenden, wenn alle Case-Ausdrücke ints, Strings oder andere eingebaute Typen mit bekannt gutem Hash-Verhalten sind, und nur dann zu versuchen, den Switch-Ausdruck zu hashen, wenn er ebenfalls von diesem Typ ist. Typobjekte sollten hier wahrscheinlich auch unterstützt werden. Dies ist das (einzige) Problem, das Schule III behandelt.
  • Bei der Optimierung mit einem Dispatch-Dict liefert die hash()-Funktion eines beteiligten Ausdrucks einen falschen Wert, und unter Schule Ib verhält sich der optimierte Code nicht wie der nicht optimierte Code. Dies ist ein bekanntes Problem mit optimierungsbezogenen Fehlern und verschwendet viel Entwicklerzeit. Unter Schule II werden in dieser Situation falsche Ergebnisse zumindest konsistent produziert, was die Fehlersuche etwas erleichtern sollte. Der für den vorherigen Punkt vorgeschlagene Ausweg würde hier auch helfen.
  • Schule Ib hat keine gute Optimierungsstrategie, wenn die Case-Ausdrücke benannte Konstanten sind. Der Compiler kann ihre Werte nicht sicher kennen und er kann nicht wissen, ob sie wirklich konstant sind. Als Ausweg wurde vorgeschlagen, den Ausdruck, der dem Case entspricht, neu auszuwerten, sobald das Dict ermittelt hat, welcher Case genommen werden soll, um zu überprüfen, ob sich der Wert des Ausdrucks nicht geändert hat. Aber streng genommen müssten auch alle Case-Ausdrücke vor diesem Case überprüft werden, um die echte if/elif-Ketten-Semantik zu bewahren, was die Optimierung vollständig zunichtemachen würde. Eine andere vorgeschlagene Lösung ist, Callbacks zu haben, die das Dispatch-Dict über Änderungen am Wert von Variablen oder Attributen informieren, die an den Case-Ausdrücken beteiligt sind. Dies ist jedoch im Allgemeinen nicht implementierbar und würde viele Namensräume dazu zwingen, die Unterstützung für solche Callbacks zu tragen, die derzeit überhaupt nicht existieren.
  • Schließlich gibt es Meinungsverschiedenheiten bezüglich der Behandlung von doppelten Fällen (d. h. zwei oder mehr Fälle mit Übereinstimmungsausdrücken, die denselben Wert ergeben). Schule I möchte dies so behandeln, wie eine if/elif-Kette es tun würde (d. h. die erste Übereinstimmung gewinnt und der Code für die zweite Übereinstimmung ist stillschweigend unerreichbar); Schule II möchte, dass dies ein Fehler zum Zeitpunkt des Einfrierens des Dispatch-Dicts ist (so dass toter Code nicht unentdeckt bleibt).

Schule I sieht Probleme im Ansatz von Schule II des Voreinfrierens eines Dispatch-Dicts, da dies Programmierern eine neue und ungewöhnliche Belastung auferlegt, genau zu verstehen, welche Arten von Case-Werten eingefroren werden dürfen und wann die Case-Werte eingefroren werden, da sie sonst vom Verhalten der Switch-Anweisung überrascht werden könnten.

Schule II glaubt nicht, dass der unoptimierte Switch von Schule Ia die Mühe wert ist, und sie sieht Probleme im Optimierungsvorschlag von Schule Ib, der dazu führen kann, dass optimierter und nicht optimierter Code unterschiedlich verhalten.

Darüber hinaus sieht Schule II wenig Wert darin, Fälle mit unhashbaren Werten zuzulassen; wenn der Benutzer solche Werte erwartet, kann er genauso gut eine if/elif-Kette schreiben. Schule II glaubt auch nicht, dass es richtig ist, toten Code aufgrund überlappender Fälle unbezeichnet zu lassen, wenn die Dict-basierte Dispatch-Implementierung es so einfach macht, dies zu erkennen.

Es gibt jedoch einige Anwendungsfälle für überlappende/doppelte Fälle. Angenommen, Sie schalten auf einigen OS-spezifischen Konstanten (z. B. exportiert vom os-Modul oder einem ähnlichen Modul). Sie haben einen Fall für jeden. Aber auf einigen OS haben zwei verschiedene Konstanten denselben Wert (da sie auf diesem OS auf die gleiche Weise implementiert sind – wie O_TEXT und O_BINARY unter Unix). Wenn doppelte Fälle als Fehler markiert werden, würde Ihr Switch auf diesem OS überhaupt nicht funktionieren. Es wäre viel besser, wenn Sie die Fälle so anordnen könnten, dass ein Fall Vorrang vor einem anderen hat.

Es gibt auch den (wahrscheinlichereren) Anwendungsfall, bei dem Sie eine Menge von Fällen haben, die gleich behandelt werden sollen, aber ein Mitglied der Menge muss anders behandelt werden. Es wäre praktisch, die Ausnahme in einem früheren Fall zu platzieren und es dabei zu belassen.

(Ja, es scheint schade zu sein, toten Code aufgrund versehentlicher Fallduplizierung nicht diagnostizieren zu können. Vielleicht ist das weniger wichtig, und pychecker kann damit umgehen? Schließlich diagnostizieren wir auch keine doppelten Methodendefinitionen.)

Dies deutet auf Schule IIb hin: wie Schule II, aber redundante Fälle müssen durch Wahl der ersten Übereinstimmung aufgelöst werden. Dies ist trivial zu implementieren, wenn das Dispatch-Dict erstellt wird (Schlüssel überspringen, die bereits vorhanden sind).

(Eine Alternative wäre die Einführung neuer Syntax, um "überlappende Fälle sind in Ordnung" oder "dieser Fall ist toter Code, wenn er nicht berücksichtigt wird" anzuzeigen, aber das halte ich für übertrieben.)

Persönlich gehöre ich zu Schule II: Ich glaube, dass der Dict-basierte Dispatch die einzig wahre Implementierung für Switch-Anweisungen ist und dass wir die Einschränkungen von vorneherein angehen sollten, damit wir maximalen Nutzen ziehen können. Ich neige zu Schule IIb – doppelte Fälle sollten durch die Reihenfolge der Fälle aufgelöst werden und nicht als Fehler markiert werden.

Wann soll das Dispatch-Dict eingefroren werden?

Für Befürworter von Schule II (Dict-basierter Dispatch) ist das nächste große Spaltungsthema, wann das für das Schalten verwendete Dict erstellt wird. Ich nenne das "Einfrieren des Dicts".

Das Hauptproblem, das dies interessant macht, ist die Beobachtung, dass Python keine benannten Compile-Zeit-Konstanten hat. Was konzeptionell eine Konstante ist, wie z. B. re.IGNORECASE, ist für den Compiler eine Variable, und nichts hindert schurkisches Verhalten daran, ihren Wert zu ändern.

Option 1

Die einschränkendste Option ist, das Dict im Compiler einzufrieren. Dies würde erfordern, dass die Case-Ausdrücke alle Literale oder Compile-Zeit-Ausdrücke sind, die nur Literale und Operatoren enthalten, deren Semantik dem Compiler bekannt ist, da mit dem aktuellen Stand von Pythons dynamischer Semantik und Einzelmodul-Kompilierung keine Hoffnung besteht, dass der Compiler die Werte von Variablen, die in solchen Ausdrücken vorkommen, mit ausreichender Sicherheit kennt. Dies wird weithin, wenn auch nicht allgemein, als zu restriktiv angesehen.

Raymond Hettinger ist der Hauptbefürworter dieses Ansatzes. Er schlägt eine Syntax vor, bei der nur ein einzelnes Literal bestimmter Typen als Case-Ausdruck zulässig ist. Es hat den Vorteil, eindeutig und leicht zu implementieren zu sein.

Mein Hauptkritikpunkt daran ist, dass wir durch die Nichtzulassung von "benannten Konstanten" Programmierer zwingen, gute Gewohnheiten aufzugeben. Benannte Konstanten werden in den meisten Sprachen eingeführt, um das Problem von "Magic Numbers" im Quellcode zu lösen. Zum Beispiel ist sys.maxint wesentlich lesbarer als 2147483647. Raymond schlägt vor, stattdessen String-Literale anstelle benannter "Enums" zu verwenden und stellt fest, dass der Inhalt des String-Literals der Name sein kann, den die Konstante sonst hätte. Somit könnten wir "case 'IGNORECASE':" anstelle von "case re.IGNORECASE:" schreiben. Wenn jedoch ein Tippfehler im String-Literal vorliegt, wird der Fall stillschweigend ignoriert, und wer weiß, wann der Fehler entdeckt wird. Wenn ein Tippfehler in einem NAMEN vorliegt, wird der Fehler jedoch sofort erkannt, wenn er ausgewertet wird. Manchmal sind die Konstanten auch extern definiert (z. B. beim Parsen eines Dateiformats wie JPEG), und wir können nicht einfach geeignete Zeichenkettenwerte wählen. Die Verwendung eines expliziten Mapping-Dicts klingt nach einem schlechten Hack.

Option 2

Der älteste Vorschlag, dies zu behandeln, ist, das Dispatch-Dict beim ersten Ausführen des Switches einzufrieren. Zu diesem Zeitpunkt können wir davon ausgehen, dass alle benannten "Konstanten" (konstant in der Vorstellung des Programmierers, aber nicht für den Compiler) als Case-Ausdrücke definiert sind – sonst hätte eine if/elif-Kette auch wenig Erfolgsaussichten. Unter der Annahme, dass der Switch viele Male ausgeführt wird, zahlt sich etwas zusätzliche Arbeit beim ersten Mal schnell durch sehr schnelle Dispatch-Zeiten danach aus.

Ein Einwand gegen diese Option ist, dass es kein offensichtliches Objekt gibt, in dem das Dispatch-Dict gespeichert werden kann. Es kann nicht im Code-Objekt gespeichert werden, das unveränderlich sein soll; es kann nicht im Funktions-Objekt gespeichert werden, da für dieselbe Funktion viele Funktions-Objekte erstellt werden können (z. B. für verschachtelte Funktionen). In der Praxis bin ich sicher, dass etwas gefunden werden kann; es könnte in einem Abschnitt des Code-Objekts gespeichert werden, der bei Vergleichen zweier Code-Objekte oder beim Pickling oder Marshalling eines Code-Objekts nicht berücksichtigt wird; oder alle Switches könnten in einem Dict gespeichert werden, das durch schwache Referenzen auf Code-Objekte indiziert ist. Die Lösung sollte auch darauf achten, Switch-Dicts nicht zwischen mehreren Interpretern zu leaken.

Ein weiterer Einwand ist, dass die "First-Use"-Regel die Obfuskation von Code wie diesem ermöglicht

def foo(x, y):
    switch x:
    case y:
        print 42

Für das ungeübte Auge (nicht vertraut mit Python) wäre dieser Code äquivalent zu diesem

def foo(x, y):
    if x == y:
        print 42

aber das ist nicht das, was er tut (es sei denn, er wird immer mit demselben Wert wie das zweite Argument aufgerufen). Dies wurde durch den Vorschlag angesprochen, dass Case-Ausdrücke keine lokalen Variablen referenzieren dürfen, aber dies ist etwas willkürlich.

Ein letzter Einwand ist, dass in einer Multi-Thread-Anwendung die "First-Use"-Regel eine komplizierte Sperrung erfordert, um die korrekte Semantik zu gewährleisten. (Die "First-Use"-Regel legt eine Verpflichtung nahe, dass Nebenwirkungen von Case-Ausdrücken genau einmal auftreten.) Dies kann so knifflig sein wie sich die Import-Sperre erwiesen hat, da die Sperre gehalten werden muss, während alle Case-Ausdrücke ausgewertet werden.

Option 3

Ein Vorschlag, der Unterstützung gewinnt (einschließlich meiner), ist, das Dict eines Switches einzufrieren, wenn die innerste Funktion, die es enthält, definiert wird. Das Switch-Dict wird im Funktions-Objekt gespeichert, genau wie Parameter-Defaults, und tatsächlich werden die Case-Ausdrücke zur selben Zeit und im selben Geltungsbereich wie die Parameter-Defaults ausgewertet (d. h. im Geltungsbereich, der die Funktionsdefinition enthält).

Diese Option hat den Vorteil, dass viele der Finessen vermieden werden, die zur Umsetzung von Option 2 erforderlich sind: Es sind keine Sperren erforderlich, keine Sorge um unveränderliche Codeobjekte oder mehrere Interpreter. Sie liefert auch eine klare Erklärung, warum Locals nicht in Case-Ausdrücken referenziert werden können.

Diese Option funktioniert genauso gut für Situationen, in denen man typischerweise einen Switch verwenden würde; Case-Ausdrücke, die importierte oder globale benannte Konstanten beinhalten, funktionieren genau wie in Option 2, solange sie vor dem Treffen der Funktionsdefinition importiert oder definiert werden.

Ein Nachteil ist jedoch, dass das Dispatch-Dict für einen Switch innerhalb einer verschachtelten Funktion jedes Mal neu berechnet werden muss, wenn die verschachtelte Funktion definiert wird. Für bestimmte "funktionale" Programmierstile kann dies den Switch in verschachtelten Funktionen unattraktiv machen. (Es sei denn, alle Case-Ausdrücke sind Compile-Zeit-Konstanten; dann steht es dem Compiler natürlich frei, den Switch-Einfrierungs-Code zu optimieren und die Dispatch-Tabelle zum Code-Objekt zu machen.)

Ein weiterer Nachteil ist, dass unter dieser Option kein klarer Moment existiert, zu dem das Dispatch-Dict für einen Switch eingefroren wird, der nicht innerhalb einer Funktion vorkommt. Es gibt ein paar pragmatische Entscheidungen, wie mit einem Switch außerhalb einer Funktion umgegangen werden soll

  1. Verbieten Sie es.
  2. Übersetzen Sie es in eine if/elif-Kette.
  3. Erlauben Sie nur Compile-Zeit-Konstante Ausdrücke.
  4. Berechnen Sie das Dispatch-Dict jedes Mal, wenn der Switch erreicht wird.
  5. Wie (b), testet aber, dass alle ausgewerteten Ausdrücke hashbar sind.

Von diesen scheint (a) zu restriktiv: es ist durchweg schlechter als (c); und (d) hat schlechte Leistung bei wenig oder keinen Vorteilen im Vergleich zu (b). Es ergibt keinen Sinn, eine leistungsfähige innere Schleife auf Modulebene zu haben, da alle lokalen Variablenreferenzen dort langsam sind; daher ist (b) mein (schwacher) Favorit. Vielleicht sollte ich (e) bevorzugen, das versucht, untypische Verwendungen eines Switches zu verhindern; Beispiele, die interaktiv, aber nicht in einer Funktion funktionieren, sind ärgerlich. Am Ende denke ich nicht, dass diese Frage so wichtig ist (außer dass sie irgendwie gelöst werden muss) und bin bereit, sie demjenigen zu überlassen, der sie am Ende implementiert.

Wenn ein Switch in einer Klasse vorkommt, aber nicht in einer Funktion, können wir das Dispatch-Dict einfrieren, wenn das temporäre Funktions-Objekt, das den Klassenrumpf darstellt, erstellt wird. Das bedeutet, dass die Case-Ausdrücke Modul-Globals, aber keine Klassenvariablen referenzieren können. Alternativ, wenn wir (b) oben wählen, könnten wir diese Implementierung auch innerhalb einer Klassendefinition wählen.

Option 4

Es gibt eine Reihe von Vorschlägen, ein Konstrukt zur Sprache hinzuzufügen, das das Konzept eines zur Funktionsdefinitionszeit vorab berechneten Wertes allgemein verfügbar macht, ohne es entweder an Parameter-Standardwerte oder Case-Ausdrücke zu binden. Einige vorgeschlagene Schlüsselwörter sind 'const', 'static', 'only' oder 'cached'. Die zugehörige Syntax und Semantik variiert.

Diese Vorschläge sind für diesen PEP nicht relevant, außer um anzudeuten, dass *wenn* ein solcher Vorschlag angenommen wird, es zwei Möglichkeiten gibt, wie der Switch davon profitieren kann: Wir könnten verlangen, dass Case-Ausdrücke entweder Compile-Zeit-Konstanten oder vorab berechnete Werte sind; oder wir könnten vorab berechnete Werte zum Standard- (und einzigen) Auswertungsmodus für Case-Ausdrücke machen. Letzteres wäre meine Präferenz, da ich keinen Nutzen für dynamischere Case-Ausdrücke sehe, der nicht bereits durch das Schreiben einer expliziten if/elif-Kette ausreichend abgedeckt wird.

Schlussfolgerung

Es ist zu früh, um zu entscheiden. Ich möchte mindestens einen abgeschlossenen Vorschlag für vorab berechnete Werte sehen, bevor ich entscheide. In der Zwischenzeit ist Python ohne Switch-Anweisung in Ordnung, und vielleicht haben diejenigen Recht, die behaupten, es wäre ein Fehler, eine hinzuzufügen.


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

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