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

Python Enhancement Proposals

PEP 275 – Umschalten auf mehrere Werte

Autor:
Marc-André Lemburg <mal at lemburg.com>
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
10. Nov. 2001
Python-Version:
2.6
Post-History:


Inhaltsverzeichnis

Ablehnungsbescheid

Eine ähnliche PEP für Python 3000, PEP 3103, wurde bereits abgelehnt, daher hat dieser Vorschlag auch keine Chance, akzeptiert zu werden.

Zusammenfassung

Diese PEP schlägt Strategien zur Verbesserung der Python-Leistung bei der Handhabung von Umschaltungen auf eine einzelne Variable mit einem von mehreren möglichen Werten vor.

Problem

Bis Python 2.5 war die übliche Art, Mehrfachwert-Schalter zu schreiben, die Verwendung langer Schalterkonstrukte des folgenden Typs

if x == 'first state':
    ...
elif x == 'second state':
    ...
elif x == 'third state':
    ...
elif x == 'fourth state':
    ...
else:
    # default handling
    ...

Dies funktioniert gut für kurze Schalterkonstrukte, da der Overhead des wiederholten Ladens eines lokalen Wertes (hier die Variable x) und dessen Vergleich mit einer Konstante gering ist (er hat im Durchschnitt eine Komplexität von O(n)). Wenn jedoch ein solches Konstrukt verwendet wird, um einen Zustandsautomaten zu schreiben, wie er für das Schreiben von Parsern benötigt wird, kann die Anzahl der möglichen Zustände leicht 10 oder mehr Fälle erreichen.

Die aktuelle Lösung für dieses Problem liegt in der Verwendung einer Dispatch-Tabelle, um die Methode des zugehörigen Falls zu finden, die je nach Wert der Schaltervariablen ausgeführt werden soll (dies kann auf eine durchschnittliche Komplexität von O(1) optimiert werden, z. B. durch die Verwendung von perfekten Hash-Tabellen). Dies funktioniert gut für Zustandsautomaten, die komplexe und langwierige Verarbeitungen in den verschiedenen Fallmethoden erfordern. Für solche, die nur eine oder zwei Anweisungen pro Fall verarbeiten, ist dies nicht performant, z. B.

def handle_data(self, data):
    self.stack.append(data)

Ein schönes Beispiel dafür ist der Zustandsautomat, der in pickle.py implementiert ist und zum Serialisieren von Python-Objekten verwendet wird. Weitere prominente Fälle sind XML SAX-Parser und Internetprotokoll-Handler.

Vorgeschlagene Lösungen

Diese PEP schlägt zwei verschiedene, aber nicht unbedingt widersprüchliche Lösungen vor

  1. Hinzufügen einer Optimierung für den Python-Compiler und die VM, die das obige if-elif-else-Konstrukt erkennt und spezielle Opcodes dafür generiert, die ein schreibgeschütztes Dictionary zum Speichern von Sprungadressen verwenden.
  2. Hinzufügen einer neuen Syntax zu Python, die die switch-Anweisung im C-Stil nachahmt.

Die erste Lösung hat den Vorteil, dass sie nicht auf das Hinzufügen neuer Schlüsselwörter zur Sprache angewiesen ist, während die zweite sauberer aussieht. Beide beinhalten einen gewissen Laufzeit-Overhead, um sicherzustellen, dass die Umschaltvariable unveränderlich und hashbar ist.

Beide Lösungen verwenden eine Dictionary-Suche, um die richtige Sprungposition zu finden, sodass sie beide denselben Problembereich teilen, da sowohl die Schaltervariable als auch die Konstanten mit der Dictionary-Implementierung kompatibel sein müssen (hashbar, vergleichbar, a==b => hash(a)==hash(b)).

Lösung 1: Optimierung von if-elif-else

Implementierung

Es sollte dem Compiler möglich sein, ein if-elif-else-Konstrukt zu erkennen, das die folgende Signatur hat

if x == 'first':...
elif x == 'second':...
else:...

d. h. die linke Seite referenziert immer dieselbe Variable, die rechte Seite einen hashbaren, unveränderlichen Builtin-Typ. Die rechten Seiten müssen nicht alle vom selben Typ sein, aber sie sollten mit dem Typ der linken Schaltervariablen vergleichbar sein.

Der Compiler könnte dann eine schreibgeschützte (perfekte) Hash-Tabelle einrichten, sie in den Konstanten speichern und einen OPCODE SWITCH vor den Standard-if-elif-else-Bytecode-Stream setzen, der das folgende Laufzeitverhalten auslöst

Zur Laufzeit prüft SWITCH, ob x einer der bekannten unveränderlichen Typen ist (Strings, Unicode, Zahlen) und verwendet die Hash-Tabelle, um den richtigen Opcode-Schnipsel zu finden. Wenn diese Bedingung nicht erfüllt ist, sollte der Interpreter auf die Standard-if-elif-else-Verarbeitung zurückgreifen, indem er einfach den SWITCH-Opcode überspringt und mit dem üblichen if-elif-else-Bytecode-Stream fortfährt.

Probleme

Die neue Optimierung sollte die aktuellen Python-Semantiken nicht ändern (indem sie die Anzahl der __cmp__-Aufrufe reduziert und __hash__-Aufrufe in if-elif-else-Konstrukten hinzufügt, die von der Optimierung betroffen sind). Um dies zu gewährleisten, kann das Umschalten nur sicher implementiert werden, entweder wenn ein Flag im Stil „from \_\_future\_\_“ verwendet wird, oder die Umschaltvariable einer der Builtin-unveränderlichen Typen ist: int, float, string, unicode, usw. (keine Subtypen, da unklar ist, ob diese noch unveränderlich sind oder nicht).

Um nachträgliche Änderungen am Sprungtabellen-Dictionary zu verhindern (die verwendet werden könnten, um auf geschützten Code zuzugreifen), muss die Sprungtabelle ein schreibgeschützter Typ sein (z. B. ein schreibgeschütztes Dictionary).

Die Optimierung sollte nur für if-elif-else-Konstrukte verwendet werden, die eine Mindestanzahl von n Fällen haben (wobei n eine Zahl ist, die noch anhand von Leistungstests definiert werden muss).

Lösung 2: Hinzufügen einer switch-Anweisung zu Python

Neue Syntax

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

(modulo Einrückungsunterschiede)

Der „else“-Teil ist optional. Wenn kein else-Teil gegeben ist und keiner der definierten Fälle zutrifft, wird keine Aktion ausgeführt und die switch-Anweisung ignoriert. Dies entspricht dem aktuellen if-Verhalten. Ein Benutzer, der diese Situation mit einer Ausnahme signalisieren möchte, kann einen else-Zweig definieren, der dann die beabsichtigte Aktion implementiert.

Beachten Sie, dass die Konstanten nicht alle vom selben Typ sein müssen, aber sie sollten mit dem Typ der Schaltervariablen vergleichbar sein.

Implementierung

Der Compiler müsste dies in Bytecode kompilieren, ähnlich wie hier

def whatis(x):
    switch(x):
        case 'one':
            print '1'
        case 'two':
            print '2'
        case 'three':
            print '3'
        else:
            print "D'oh!"

in (POP_TOPs und SET_LINENOs weggelassen)

   6  LOAD_FAST         0 (x)
   9  LOAD_CONST        1 (switch-table-1)
  12  SWITCH            26 (to 38)

  14  LOAD_CONST        2 ('1')
  17  PRINT_ITEM
  18  PRINT_NEWLINE
  19  JUMP 43

  22  LOAD_CONST        3 ('2')
  25  PRINT_ITEM
  26  PRINT_NEWLINE
  27  JUMP 43

  30  LOAD_CONST        4 ('3')
  33  PRINT_ITEM
  34  PRINT_NEWLINE
  35  JUMP 43

  38  LOAD_CONST        5 ("D'oh!")
  41  PRINT_ITEM
  42  PRINT_NEWLINE

>>43  LOAD_CONST        0 (None)
  46  RETURN_VALUE

Wobei der ‘SWITCH’-Opcode je nach „x“ zu 14, 22, 30 oder 38 springen würde.

Thomas Wouters hat einen Patch geschrieben, der das oben beschriebene demonstriert. Sie können ihn von [1] herunterladen.

Probleme

Die switch-Anweisung sollte kein Fall-Through-Verhalten implementieren (wie die switch-Anweisung in C). Jeder Fall definiert eine vollständige und unabhängige Suite; ähnlich wie in einer if-elif-else-Anweisung. Dies ermöglicht auch die Verwendung von break in switch-Anweisungen innerhalb von Schleifen.

Wenn der Interpreter feststellt, dass die Schaltervariable x nicht hashbar ist, sollte er zur Laufzeit einen TypeError auslösen, der das Problem aufzeigt.

Es gab andere Vorschläge für die Syntax, die vorhandene Schlüsselwörter wiederverwenden und das Hinzufügen von zwei neuen vermeiden („switch“ und „case“). Andere argumentierten, dass die Schlüsselwörter neue Begriffe verwenden sollten, um Verwirrung mit den C-Schlüsselwörtern gleichen Namens, aber leicht unterschiedlicher Semantik zu vermeiden (z. B. Fall-Through ohne break). Einige der vorgeschlagenen Varianten

case EXPR:
    of CONSTANT:
        SUITE
    of CONSTANT:
        SUITE
    else:
        SUITE

case EXPR:
    if CONSTANT:
         SUITE
    if CONSTANT:
        SUITE
    else:
        SUITE

when EXPR:
    in CONSTANT_TUPLE:
        SUITE
    in CONSTANT_TUPLE:
        SUITE
    ...
else:
     SUITE

Die switch-Anweisung könnte erweitert werden, um mehrere Werte für einen Abschnitt zuzulassen (z. B. case ‘a’, ‘b’, ‘c’: …). Eine weitere vorgeschlagene Erweiterung würde Wertebereiche erlauben (z. B. case 10..14: …). Diese sollten wahrscheinlich aufgeschoben werden, aber bereits bei der Konzeption und Implementierung einer ersten Version berücksichtigt werden.

Beispiele

Die folgenden Beispiele verwenden alle eine neue Syntax, wie in Lösung 2 vorgeschlagen. Alle diese Beispiele würden jedoch auch mit Lösung 1 funktionieren.

switch EXPR:                   switch x:
    case CONSTANT:                 case "first":
        SUITE                          print x
    case CONSTANT:                 case "second":
        SUITE                          x = x**2
    ...                                print x
    else:                          else:
        SUITE                          print "whoops!"


case EXPR:                     case x:
    of CONSTANT:                   of "first":
        SUITE                          print x
    of CONSTANT:                   of "second":
        SUITE                          print x**2
    else:                          else:
        SUITE                          print "whoops!"


case EXPR:                     case state:
    if CONSTANT:                   if "first":
         SUITE                         state = "second"
    if CONSTANT:                   if "second":
        SUITE                          state = "third"
    else:                          else:
        SUITE                          state = "first"


when EXPR:                     when state:
    in CONSTANT_TUPLE:             in ("first", "second"):
        SUITE                          print state
    in CONSTANT_TUPLE:                 state = next_state(state)
        SUITE                      in ("seventh",):
    ...                                print "done"
else:                                  break    # out of loop!
     SUITE                     else:
                                   print "middle state"
                                   state = next_state(state)

Hier ist eine weitere schöne Anwendung, die von Jack Jansen gefunden wurde (Umschalten nach Argumenttypen)

switch type(x).__name__:
    case 'int':
        SUITE
    case 'string':
        SUITE

Umfang

XXX „from \_\_future\_\_ import switch“ erklären

Danksagungen

  • Martin von Löwis (Probleme mit der Optimierungsidee)
  • Thomas Wouters (switch-Anweisung + Bytecode-Compiler-Beispiel)
  • Skip Montanaro (Dispatching-Ideen, Beispiele)
  • Donald Beaudry (switch-Syntax)
  • Greg Ewing (switch-Syntax)
  • Jack Jansen (Typ-Switching-Beispiele)

Referenzen


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

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