PEP 463 – Ausnahmebehandlungs-Ausdrücke
- Autor:
- Chris Angelico <rosuav at gmail.com>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 15. Feb 2014
- Python-Version:
- 3.5
- Post-History:
- 20. Feb 2014, 16. Feb 2014
- Resolution:
- Python-Dev Nachricht
Ablehnungsbescheid
Von https://mail.python.org/pipermail/python-dev/2014-March/133118.html
“””Ich möchte diesen PEP ablehnen. Ich denke, die vorgeschlagene Syntax ist angesichts der gewünschten Semantik akzeptabel, obwohl sie immer noch etwas befremdlich ist. Sie ist wahrscheinlich nicht schlimmer als der Doppelpunkt bei Lambda (der den Doppelpunkt in einem def widerspiegelt, so wie der Doppelpunkt hier den in einem try/except widerspiegelt) und definitiv besser als die aufgeführten Alternativen.
Aber die Motivation und Begründung, hinter der ich nicht stehen kann. Ich glaube nicht, dass z.B. dict.get() unnötig wird, sobald wir except-Ausdrücke haben, und ich stimme der Position nicht zu, dass EAFP besser als LBYL ist oder von Python „im Allgemeinen empfohlen“ wird. (Woher haben Sie das? Von denselben Quellen, die so besessen von DRY sind, dass sie lieber eine Funktion höherer Ordnung einführen, als eine Codezeile zu wiederholen? :-)
Dies ist wahrscheinlich das Höchste, was ich an Erklärungen von mir geben kann. Da der Sprachgipfel bevorsteht, bin ich gerne bereit, meine Gründe für die Ablehnung dort näher zu erläutern (falls Bedarf besteht).
Ich denke, dass dies (abgesehen davon, dass diese schrecklichen Akronyme nie erklärt werden :-) ein gut geschriebener und gut recherchierter PEP war, und ich denke, Sie haben großartige Arbeit geleistet, die Diskussion zu moderieren, Einwände zu sammeln, Alternativen zu prüfen und alles andere, was erforderlich ist, um eine hitzige Debatte in einen PEP zu verwandeln. Gut gemacht Chris (und alle, die geholfen haben), und viel Glück mit Ihrem nächsten PEP! “””
Zusammenfassung
So wie PEP 308 ein Mittel für wertbasierte Bedingungen in einem Ausdruck einführte, ermöglicht dieses System ausnahmebasierte Bedingungen als Teil eines Ausdrucks zu verwenden.
Motivation
Eine Reihe von Funktionen und Methoden haben Parameter, die dazu führen, dass sie einen angegebenen Wert zurückgeben, anstatt eine Ausnahme auszulösen. Das aktuelle System ist ad hoc und inkonsistent und erfordert, dass jede Funktion einzeln mit dieser Funktionalität geschrieben wird; nicht alle unterstützen dies.
- dict.get(key, default) – zweites Positionsargument anstelle von KeyError
- next(iter, default) – zweites Positionsargument anstelle von StopIteration
- list.pop() – keine Möglichkeit, einen Standardwert zurückzugeben
- seq[index] – keine Möglichkeit, einen Grenzfehler zu behandeln
- min(sequence, default=default) – Schlüsselwortargument anstelle von ValueError
- statistics.mean(data) – keine Möglichkeit, einen leeren Iterator zu behandeln
Hätte es diese Möglichkeit früh in der Geschichte von Python gegeben, hätte man dict.get() und verwandte Methoden nicht erstellen müssen; die einzig offensichtliche Methode, eine fehlende Schlüssel zu behandeln, wäre die Reaktion auf die Ausnahme gewesen. Eine Methode wird geschrieben, die die Abwesenheit auf eine Weise signalisiert, und eine konsistente Technik wird verwendet, um auf die Abwesenheit zu reagieren. Stattdessen haben wir dict.get(), und ab Python 3.4 haben wir auch min(… default=default) und unzählige andere. Wir haben eine LBYL-Syntax zum Testen innerhalb eines Ausdrucks, aber derzeit keine EAFP-Notation; vergleichen Sie das Folgende
# LBYL:
if key in dic:
process(dic[key])
else:
process(None)
# As an expression:
process(dic[key] if key in dic else None)
# EAFP:
try:
process(dic[key])
except KeyError:
process(None)
# As an expression:
process(dic[key] except KeyError: None)
Python empfiehlt im Allgemeinen die EAFP-Richtlinie, muss dann aber Hilfsfunktionen wie dic.get(key,None) verwenden, um dies zu ermöglichen.
Begründung
Das aktuelle System erfordert, dass ein Funktionsautor den Bedarf an einem Standardwert vorhersagt und dessen Unterstützung implementiert. Wenn dies nicht geschieht, ist ein vollständiger try/except-Block erforderlich.
Da try/except eine Anweisung ist, ist es unmöglich, Ausnahmen mitten in einem Ausdruck abzufangen. So wie if/else für Bedingungen und lambda für Funktionsdefinitionen, ermöglicht dies das Abfangen von Ausnahmen in einem Ausdruckskontext.
Dies bietet eine saubere und konsistente Möglichkeit für eine Funktion, einen Standardwert bereitzustellen: Sie löst einfach eine entsprechende Ausnahme aus, und der Aufrufer fängt sie ab.
In einigen Situationen kann eine LBYL-Technik angewendet werden (z. B. Prüfung, ob eine Sequenz genügend Länge hat, bevor darauf zugegriffen wird). Dies ist nicht in allen Fällen sicher, aber da es oft bequem ist, werden Programmierer versucht sein, die Sicherheit von EAFP zugunsten der notationellen Kürze von LBYL zu opfern. Darüber hinaus verzerren einige LBYL-Techniken (z. B. bei getattr mit drei Argumenten) den Code so, dass er wie reine Zeichenketten aussieht, anstatt wie Attributzugriffe, was die Lesbarkeit beeinträchtigen kann. Eine bequeme EAFP-Notation löst all dies.
Es gibt keine bequeme Möglichkeit, eine Hilfsfunktion zu schreiben, um dies zu tun; das Nächstliegende ist etwas Hässliches, das entweder lambda verwendet
def except_(expression, exception_list, default):
try:
return expression()
except exception_list:
return default()
value = except_(lambda: 1/x, ZeroDivisionError, lambda: float("nan"))
was umständlich ist und keine mehreren Ausnahmebehandlungen verarbeiten kann; oder eval
def except_(expression, exception_list, default):
try:
return eval(expression, globals_of_caller(), locals_of_caller())
except exception_list as exc:
l = locals_of_caller().copy()
l['exc'] = exc
return eval(default, globals_of_caller(), l)
def globals_of_caller():
return sys._getframe(2).f_globals
def locals_of_caller():
return sys._getframe(2).f_locals
value = except_("""1/x""",ZeroDivisionError,""" "Can't divide by zero" """)
was noch umständlicher ist und auf implementierungsabhängige Hacks angewiesen ist. (Das Schreiben von globals_of_caller() und locals_of_caller() für Interpreter, die keine CPython sind, bleibt als Übung für den Leser übrig.)
Raymond Hettinger äußert den Wunsch nach einer solchen konsistenten API. Etwas Ähnliches wurde bereits mehrfach in der Vergangenheit angefordert.
Vorschlag
So wie der „or“-Operator und der dreiteilige „if-else“-Ausdruck kurzschließende Methoden zum Abfangen eines falsy-Wertes und dessen Ersetzen bieten, bietet diese Syntax eine kurzschließende Methode zum Abfangen einer Ausnahme und deren Ersetzen.
Dies funktioniert derzeit
lst = [1, 2, None, 3]
value = lst[2] or "No value"
Der Vorschlag fügt dies hinzu
lst = [1, 2]
value = (lst[2] except IndexError: "No value")
Insbesondere ist die vorgeschlagene Syntax
(expr except exception_list: default)
wobei expr, exception_list und default alle Ausdrücke sind. Zuerst wird expr ausgewertet. Wenn keine Ausnahme ausgelöst wird, ist sein Wert der Wert des gesamten Ausdrucks. Wenn eine Ausnahme ausgelöst wird, wird exception_list ausgewertet und sollte entweder einen Typ oder ein Tupel ergeben, genau wie bei der Anweisungsform von try/except. Jede übereinstimmende Ausnahme führt dazu, dass der entsprechende default-Ausdruck ausgewertet wird und zum Wert des Ausdrucks wird. Wie bei der Anweisungsform von try/except breiten sich nicht übereinstimmende Ausnahmen nach oben aus.
Klammern sind um den gesamten Ausdruck erforderlich, es sei denn, sie wären vollständig redundant, gemäß denselben Regeln wie Generator-Ausdrücke. Dies garantiert die korrekte Interpretation verschachtelter except-Ausdrücke und ermöglicht zukünftige Erweiterungen der Syntax – siehe unten zu mehreren except-Klauseln.
Beachten Sie, dass der aktuelle Vorschlag das Erfassen des Ausnahmeobjekts nicht zulässt. Wo dies erforderlich ist, muss die Anweisungsform verwendet werden. (Siehe unten für Diskussion und Erläuterung hierzu.)
Dieser ternäre Operator würde in der Rangfolge zwischen lambda und if/else liegen.
Betrachten Sie dieses Beispiel eines zweistufigen Caches
for key in sequence:
x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
# do something with x
Dies kann nicht umgeschrieben werden als
x = lvl1.get(key, lvl2.get(key, f(key)))
was, obwohl kürzer, den Zweck des Caches untergräbt, da es einen Standardwert berechnen muss, um ihn an get() zu übergeben. Die .get()-Version berechnet rückwärts; die Ausnahme-Testversion berechnet vorwärts, wie erwartet. Das nächstliegende nützliche Äquivalent wäre
x = lvl1.get(key) or lvl2.get(key) or f(key)
was von den Werten ungleich Null abhängt und auch davon abhängt, dass das Cache-Objekt diese Funktionalität unterstützt.
Alternative Vorschläge
Diskussionen über python-ideas brachten die folgenden Syntaxvorschläge hervor
value = expr except default if Exception [as e]
value = expr except default for Exception [as e]
value = expr except default from Exception [as e]
value = expr except Exception [as e] return default
value = expr except (Exception [as e]: default)
value = expr except Exception [as e] try default
value = expr except Exception [as e] continue with default
value = default except Exception [as e] else expr
value = try expr except Exception [as e]: default
value = expr except default # Catches anything
value = expr except(Exception) default # Catches only the named type(s)
value = default if expr raise Exception
value = expr or else default if Exception
value = expr except Exception [as e] -> default
value = expr except Exception [as e] pass default
Es wurde auch vorgeschlagen, ein neues Schlüsselwort zu erstellen, anstatt ein vorhandenes wiederzuverwenden. Solche Vorschläge fallen in dieselbe Struktur wie die letzte Form, aber mit einem anderen Schlüsselwort anstelle von ‚pass‘. Vorschläge sind ‚then‘, ‚when‘ und ‚use‘. Auch im Kontext des Vorschlags „default if expr raise Exception“ wurde vorgeschlagen, ein neues Schlüsselwort „raises“ zu verwenden.
Alle Formen, die die ‚as‘-Erfassungsklausel beinhalten, wurden im Interesse der Einfachheit von diesem Vorschlag zurückgestellt, sind aber in der obigen Tabelle als genaue Aufzeichnung von Vorschlägen erhalten geblieben.
Die vier am stärksten von diesem Vorschlag unterstützten Formen sind in der Reihenfolge
value = (expr except Exception: default)
value = (expr except Exception -> default)
value = (expr except Exception pass default)
value = (expr except Exception then default)
Alle vier behalten die Auswertungsreihenfolge von links nach rechts bei: zuerst der Basis-Ausdruck, dann die Ausnahme-Liste und zuletzt der Standardwert. Dies ist wichtig, da die Ausdrücke verzögert ausgewertet werden. Im Vergleich dazu müssen mehrere der oben aufgeführten ad-hoc-Alternativen (durch die Natur von Funktionen) ihre Standardwerte eilig auswerten. Die bevorzugte Form, die den Doppelpunkt verwendet, parallelisiert try/except, indem sie „except exception_list:“ verwendet, und parallelisiert lambda, indem sie „keyword name_list: subexpression“ hat; sie kann auch als Abbildung von Exception auf den Standardwert gelesen werden, im Stil eines Wörterbuchs. Die Verwendung des Pfeils führt ein Token ein, mit dem viele Programmierer nicht vertraut sein werden und das derzeit keine ähnliche Bedeutung hat, aber ansonsten recht lesbar ist. Das englische Wort „pass“ hat eine vage ähnliche Bedeutung (man denke an die übliche Verwendung von „pass by value/reference“ für Funktionsargumente), und „pass“ ist bereits ein Schlüsselwort, aber da seine Bedeutung deutlich unzusammenhängend ist, kann dies zu Verwirrung führen. Die Verwendung von „then“ ergibt auf Englisch Sinn, führt aber ein neues Schlüsselwort in die Sprache ein – wenn auch eines, das nicht häufig verwendet wird, aber dennoch ein neues Schlüsselwort.
Die Auswertungsreihenfolge von links nach rechts ist für die Lesbarkeit extrem wichtig, da sie der Reihenfolge entspricht, in der die meisten Ausdrücke ausgewertet werden. Alternativen wie
value = (expr except default if Exception)
brechen dies, indem sie zuerst die beiden Enden auswerten und dann zur Mitte kommen; während dies nicht schlimm erscheinen mag (da die Ausnahme-Liste normalerweise konstant ist), fügt es Verwirrung hinzu, wenn mehrere Klauseln aufeinandertreffen, entweder mit mehreren except/if oder mit dem vorhandenen if/else oder einer Kombination. Bei der bevorzugten Reihenfolge werden Unterausdrücke immer von links nach rechts ausgewertet, unabhängig davon, wie die Syntax verschachtelt ist.
Beibehaltung der bestehenden Notation, aber Verschiebung der obligatorischen Klammern, erhalten wir den folgenden Vorschlag
value = expr except (Exception: default)
value = expr except(Exception: default)
Dies erinnert an einen Funktionsaufruf oder eine Wörterbuchinitialisierung. Der Doppelpunkt kann nicht mit der Einleitung einer Suite verwechselt werden, aber andererseits garantiert die neue Syntax die verzögerte Auswertung, was ein Wörterbuch nicht tut. Das Potenzial zur Reduzierung von Verwirrung wird durch das entsprechende Potenzial zur Erhöhung als ungerechtfertigt angesehen.
Beispielhafte Nutzung
Für jedes Beispiel wird eine ungefähr gleichwertige Anweisungsform angegeben, um zu zeigen, wie der Ausdruck geparst wird. Diese sind nicht immer streng äquivalent, werden aber denselben Zweck erfüllen. Es ist NICHT sicher für den Interpreter, eine in die andere zu übersetzen.
Eine Reihe dieser Beispiele stammen direkt aus der Standardbibliothek von Python, mit Dateinamen und Zeilennummern korrekt ab Anfang Februar 2014. Viele dieser Muster sind extrem verbreitet.
Ein Argument abrufen, standardmäßig None
cond = (args[1] except IndexError: None)
# Lib/pdb.py:803:
try:
cond = args[1]
except IndexError:
cond = None
Informationen vom System abrufen, falls verfügbar
pwd = (os.getcwd() except OSError: None)
# Lib/tkinter/filedialog.py:210:
try:
pwd = os.getcwd()
except OSError:
pwd = None
Einen Übersetzungsversuch unternehmen, mit dem Original als Fallback
e.widget = (self._nametowidget(W) except KeyError: W)
# Lib/tkinter/__init__.py:1222:
try:
e.widget = self._nametowidget(W)
except KeyError:
e.widget = W
Aus einem Iterator lesen, mit leeren Zeilen fortfahren, sobald dieser erschöpft ist
line = (readline() except StopIteration: '')
# Lib/lib2to3/pgen2/tokenize.py:370:
try:
line = readline()
except StopIteration:
line = ''
Plattformspezifische Informationen abrufen (beachten Sie die DRY-Verbesserung); dieses spezielle Beispiel könnte weiter ausgebaut werden, indem eine Reihe separater Zuweisungen in eine einzige große Wörterbuchinitialisierung umgewandelt werden
# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = (sys.abiflags except AttributeError: '')
# Lib/sysconfig.py:529:
try:
_CONFIG_VARS['abiflags'] = sys.abiflags
except AttributeError:
# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = ''
Einen indizierten Eintrag abrufen, standardmäßig None (ähnlich wie dict.get)
def getNamedItem(self, name):
return (self._attrs[name] except KeyError: None)
# Lib/xml/dom/minidom.py:573:
def getNamedItem(self, name):
try:
return self._attrs[name]
except KeyError:
return None
Zahlen in Namen übersetzen, mit den Zahlen als Fallback
g = (grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid)
u = (pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid)
# Lib/tarfile.py:2198:
try:
g = grp.getgrnam(tarinfo.gname)[2]
except KeyError:
g = tarinfo.gid
try:
u = pwd.getpwnam(tarinfo.uname)[2]
except KeyError:
u = tarinfo.uid
Ein Attribut nachschlagen, mit einem Standardwert als Fallback
mode = (f.mode except AttributeError: 'rb')
# Lib/aifc.py:882:
if hasattr(f, 'mode'):
mode = f.mode
else:
mode = 'rb'
return (sys._getframe(1) except AttributeError: None)
# Lib/inspect.py:1350:
return sys._getframe(1) if hasattr(sys, "_getframe") else None
Einige langwierige Berechnungen im EAFP-Modus durchführen, wobei die Division durch Null als eine Art klebriges NaN behandelt wird
value = (calculate(x) except ZeroDivisionError: float("nan"))
try:
value = calculate(x)
except ZeroDivisionError:
value = float("nan")
Den Durchschnitt einer Reihe von Zahlen berechnen, mit Null als Fallback
value = (statistics.mean(lst) except statistics.StatisticsError: 0)
try:
value = statistics.mean(lst)
except statistics.StatisticsError:
value = 0
Objekte in einer spärlichen Liste von Überschreibungen nachschlagen
(overrides[x] or default except IndexError: default).ping()
try:
(overrides[x] or default).ping()
except IndexError:
default.ping()
Einschränkung des Geltungsbereichs der Ausnahmebehandlung
Die folgenden Beispiele, die direkt aus der Standardbibliothek von Python stammen, zeigen, wie der Geltungsbereich von try/except bequem eingeschränkt werden kann. Dies mit der Anweisungsform von try/except zu tun, würde eine temporäre Variable erfordern, aber es ist als Ausdruck weit sauberer.
Lib/ipaddress.py:343
try:
ips.append(ip.ip)
except AttributeError:
ips.append(ip.network_address)
Wird zu
ips.append(ip.ip except AttributeError: ip.network_address)
Die Ausdrucksform ist fast äquivalent zu dieser
try:
_ = ip.ip
except AttributeError:
_ = ip.network_address
ips.append(_)
Lib/tempfile.py:130
try:
dirlist.append(_os.getcwd())
except (AttributeError, OSError):
dirlist.append(_os.curdir)
Wird zu
dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)
Lib/asyncore.py:264
try:
status.append('%s:%d' % self.addr)
except TypeError:
status.append(repr(self.addr))
Wird zu
status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
In jedem Fall stellt der eingeschränkte Geltungsbereich von try/except sicher, dass eine unerwartete Ausnahme (z. B. AttributeError, wenn „append“ falsch geschrieben wäre) nicht vom selben Handler abgefangen wird. Dies ist unwahrscheinlich genug, um den Aufruf in eine separate Zeile auszulagern (gemäß dem oben genannten Beispiel mit fünf Zeilen), aber es ist ein kleiner Vorteil, der sich als Nebeneffekt der Konvertierung ergibt.
Vergleiche mit anderen Sprachen
(Mit Dank an Andrew Barnert für die Zusammenstellung dieses Abschnitts. Beachten Sie, dass die hier gegebenen Beispiele nicht die aktuelle Version des Vorschlags widerspiegeln und bearbeitet werden müssen.)
Rubys „begin…rescue…rescue…else…ensure…end“ ist ein Ausdruck (potenziell mit Anweisungen darin). Er hat das Äquivalent einer „as“-Klausel und das Äquivalent von bare except. Und er verwendet keine Satzzeichen oder Schlüsselwörter zwischen dem bare except/Exception-Klasse/Exception-Klasse mit as-Klausel und dem Wert. (Und ja, er ist mehrdeutig, es sei denn, man versteht Rubys Anweisungs-/Ausdrucksregeln.)
x = begin computation() rescue MyException => e default(e) end;
x = begin computation() rescue MyException default() end;
x = begin computation() rescue default() end;
x = begin computation() rescue MyException default() rescue OtherException other() end;
Im Sinne dieses PEP
x = computation() except MyException as e default(e)
x = computation() except MyException default(e)
x = computation() except default(e)
x = computation() except MyException default() except OtherException other()
Erlang hat einen try-Ausdruck, der so aussieht
x = try computation() catch MyException:e -> default(e) end;
x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;
Die Klasse und der „as“-Name sind obligatorisch, aber man kann „_“ für beide verwenden. Es gibt auch eine optionale „when“-Güte bei jeder und eine „throw“-Klausel, die man abfangen kann, auf die ich nicht näher eingehen werde. Um mehrere Ausnahmen zu behandeln, trennt man die Klauseln einfach durch Semikolons, was in Python wohl Kommas entsprechen würde. Also
x = try computation() except MyException as e -> default(e)
x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)
Erlang hat auch einen „catch“-Ausdruck, der trotz der Verwendung desselben Schlüsselworts völlig anders ist, und Sie möchten ihn nicht kennen.
Die ML-Familie hat zwei verschiedene Möglichkeiten, damit umzugehen: „handle“ und „try“; der Unterschied zwischen beiden besteht darin, dass „try“ die Ausnahme einem Mustervergleich unterzieht, was Ihnen den Effekt mehrerer except-Klauseln und as-Klauseln gibt. In beiden Formen wird die Handler-Klausel in einigen Dialekten durch „=>“ und in anderen durch „->“ unterteilt.
Um Verwirrung zu vermeiden, schreibe ich die Funktionsaufrufe im Python-Stil.
Hier ist SMLs „handle“
let x = computation() handle MyException => default();;
Hier ist OCamls „try“
let x = try computation() with MyException explanation -> default(explanation);;
let x = try computation() with
MyException(e) -> default(e)
| MyOtherException() -> other_default()
| (e) -> fallback(e);;
Im Sinne dieses PEP wären dies etwa
x = computation() except MyException => default()
x = try computation() except MyException e -> default()
x = (try computation()
except MyException as e -> default(e)
except MyOtherException -> other_default()
except BaseException as e -> fallback(e))
Viele ML-inspirierte, aber nicht direkt verwandte Sprachen aus der akademischen Welt vermischen die Dinge, verwenden normalerweise mehr Schlüsselwörter und weniger Symbole. So würde das Oz in Python als
x = try computation() catch MyException as e then default(e)
Viele Lisp-abgeleitete Sprachen, wie Clojure, implementieren try/catch als Sonderformen (wenn Sie nicht wissen, was das bedeutet, denken Sie an funktionsähnliche Makros), so dass man im Wesentlichen schreibt
try(computation(), catch(MyException, explanation, default(explanation)))
try(computation(),
catch(MyException, explanation, default(explanation)),
catch(MyOtherException, explanation, other_default(explanation)))
In Common Lisp wird dies mit einem etwas umständlicheren „handler-case“-Makro gemacht, aber die Grundidee ist dieselbe.
Der Lisp-Stil wird überraschenderweise von einigen Sprachen verwendet, die keine Makros haben, wie Lua, wo xpcall Funktionen annimmt. Lambdas im Python-Stil anstelle von Lua-Stil schreiben
x = xpcall(lambda: expression(), lambda e: default(e))
Dies gibt tatsächlich (true, expression()) oder (false, default(e)) zurück, aber ich denke, wir können diesen Teil ignorieren.
Haskell ist hier eigentlich ähnlich wie Lua (außer dass alles über Monaden läuft, natürlich)
x = do catch(lambda: expression(), lambda e: default(e))
Man kann einen Mustervergleichsausdruck innerhalb der Funktion schreiben, um zu entscheiden, was damit zu tun ist; das Abfangen und erneute Auslösen von Ausnahmen, die man nicht möchte, ist billig genug, um idiomatisch zu sein.
Aber Haskell-Infixierung macht dies schöner
x = do expression() `catch` lambda: default()
x = do expression() `catch` lambda e: default(e)
Und das macht den Vergleich zwischen dem Lambda-Doppelpunkt und dem Except-Doppelpunkt im Vorschlag viel offensichtlicher
x = expression() except Exception: default()
x = expression() except Exception as e: default(e)
Tcl hat die andere Hälfte von Luas xpcall; catch ist eine Funktion, die true zurückgibt, wenn eine Ausnahme abgefangen wurde, false andernfalls, und den Wert auf andere Weise erhält. Und es ist alles auf dem impliziten Zitat-und-Ausführung aufgebaut, auf dem alles in Tcl basiert, was es noch schwieriger macht, es in Python-Begriffen zu beschreiben als Lisp-Makros, aber so etwas wie
if {[ catch("computation()") "explanation"]} { default(explanation) }
Smalltalk ist auch schwer auf Python zu übertragen. Die Grundversion wäre
x := computation() on:MyException do:default()
… aber das ist im Grunde Smalltalks Syntax für das Übergeben von Argumenten mit Doppelpunkten, nicht seine Syntax für die Ausnahmebehandlung.
Zurückgestellte Untervorschläge
Mehrere except-Klauseln
Eine Untersuchung von Anwendungsfällen zeigt, dass dies nicht so oft benötigt wird wie mit der Anweisungsform, und da seine Syntax ein Punkt ist, über den noch kein Konsens erzielt wurde, wird die gesamte Funktion zurückgestellt.
Mehrere „except“-Schlüsselwörter könnten verwendet werden, und sie werden alle Ausnahmen abfangen, die im ursprünglichen Ausdruck ausgelöst werden (nur)
# Will catch any of the listed exceptions thrown by expr;
# any exception thrown by a default expression will propagate.
value = (expr
except Exception1: default1
except Exception2: default2
# ... except ExceptionN: defaultN
)
Derzeit muss eine der folgenden Formen verwendet werden
# Will catch an Exception2 thrown by either expr or default1
value = (
(expr except Exception1: default1)
except Exception2: default2
)
# Will catch an Exception2 thrown by default1 only
value = (expr except Exception1:
(default1 except Exception2: default2)
)
Das Auflisten mehrerer Ausnahmebehandlungsklauseln ohne Klammern ist ein Syntaxfehler (siehe oben), und so ist eine zukünftige Version von Python frei, diese Funktion hinzuzufügen, ohne vorhandenen Code zu brechen.
Erfassung des Ausnahmeobjekts
In einem try/except-Block erstellt die Verwendung von ‚as‘ zum Erfassen des Ausnahmeobjekts eine lokale Namensbindung und löscht diese Bindung implizit (um eine Referenzschleife zu vermeiden) in einer finally-Klausel. Im Ausdruckskontext macht dies wenig Sinn, und ein ordnungsgemäßer Unter-Scope wäre erforderlich, um das Ausnahmeobjekt sicher zu erfassen – etwas Ähnliches wie bei einer Listenkomprehension gehandhabt wird. CPython implementiert derzeit den Unter-Scope einer Komprehension mit einem verschachtelten Funktionsaufruf, was in einigen Kontexten wie Klassendefinitionen Konsequenzen hat und daher für diesen Vorschlag ungeeignet ist. Sollte es in Zukunft eine Möglichkeit geben, einen echten Unter-Scope zu erstellen (was Komprehensionen, Except-Ausdrücke, mit-Blöcke und möglicherweise mehr vereinfachen könnte), dann könnte dieser Vorschlag wiederbelebt werden; bis dahin ist sein Verlust kein großer, da die einfache Ausnahmebehandlung, die sich für die hier verwendete Ausdrucksnotation gut eignet, im Allgemeinen nur den Typ der Ausnahme und nicht ihren Wert betrifft – weitere Analyse unten.
Diese Syntax würde zugegebenermaßen eine bequeme Möglichkeit bieten, Ausnahmen im interaktiven Python zu erfassen; zurückgegebene Werte werden von „_“ erfasst, Ausnahmen jedoch derzeit nicht. Dies könnte geschrieben werden als
>>> (expr except Exception as e: e)
Eine Untersuchung der Python-Standardbibliothek zeigt, dass die Verwendung von ‚as‘ zwar ziemlich verbreitet ist (etwa in jeder fünften except-Klausel), aber extrem **unüblich** in den Fällen, die logischerweise in die Ausdrucksform umgewandelt werden könnten. Ihre wenigen Verwendungen können einfach unverändert bleiben. Daher habe ich im Interesse der Einfachheit auf die Aufnahme der ‚as‘-Klausel in diesen Vorschlag verzichtet. Eine spätere Python-Version kann dies hinzufügen, ohne bestehenden Code zu brechen, da ‚as‘ bereits ein Schlüsselwort ist.
Ein Beispiel, wo dies möglicherweise nützlich sein könnte, ist Lib/imaplib.py:568
try: typ, dat = self._simple_command('LOGOUT')
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Dies könnte werden
typ, dat = (self._simple_command('LOGOUT')
except BaseException as e: ('NO', '%s: %s' % (type(e), e)))
Oder vielleicht eine andere Variante. Dies ist kaum der überzeugendste Anwendungsfall, aber ein intelligenter Blick auf diesen Code könnte ihn erheblich aufräumen. In Ermangelung weiterer Beispiele, die einen Bedarf am Ausnahmeobjekt zeigen, habe ich mich entschieden, die Empfehlung auf unbestimmte Zeit zu verschieben.
Abgelehnte Untervorschläge
finally-Klausel
Die Anweisungsformen try… finally oder try… except… finally haben keine logische entsprechende Ausdrucksform. Daher ist das finally-Schlüsselwort in keiner Weise Teil dieses Vorschlags.
Leeres except mit anderer Bedeutung
Bei mehreren der vorgeschlagenen Syntaxen wäre das Weglassen des Ausnahmetypnamens einfach und prägnant und wäre verlockend. Aus Bequemlichkeitsgründen könnte es vorteilhaft sein, eine leere ‚except‘-Klausel zu haben, die eine nützlichere Bedeutung hat als „except BaseException“. Vorschläge enthielten, dass sie Exception abfängt, oder eine bestimmte Menge von „gängigen Ausnahmen“ (Unterklassen eines neuen Typs namens ExpressionError), oder dass sie nach einem Tupel namens ExpressionError im aktuellen Geltungsbereich sucht, mit einem eingebauten Standard wie (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError). All dies wurde aus mehreren Gründen abgelehnt.
- Erstens und vor allem würde die Konsistenz mit der Anweisungsform von try/except gebrochen. So wie eine Listenkomprehension oder ein ternärer if-Ausdruck durch „Aufbrechen“ in seine vertikale Anweisungsform erklärt werden kann, sollte ein Expression-Except durch eine relativ mechanische Übersetzung in eine nahezu äquivalente Anweisung erklärt werden können. Jede Form von Syntax, die beiden gemeinsam ist, sollte daher in jeder dieselbe Semantik haben und vor allem nicht den subtilen Unterschied haben, dass sie in einer mehr als in der anderen abfängt, da dies zu unbemerkten Fehlern führen wird.
- Zweitens wäre die Menge der geeigneten abzufangenden Ausnahmen selbst ein großer Streitpunkt. Es wäre unmöglich vorherzusagen, welche Ausnahmen „sinnvoll“ abzufangen wären; warum einige von ihnen mit bequemer Syntax segnen und andere nicht?
- Und schließlich (dies teilweise, weil die Empfehlung war, dass ein bare except aktiv gefördert werden sollte, sobald es auf eine „vernünftige“ Menge von Ausnahmen reduziert wurde), ist jede Situation, in der Sie eine unerwartete Ausnahme abfangen, ein unnötiger Fehler-Magnet.
Folglich ist die Verwendung eines leeren ‚except‘ auf zwei Möglichkeiten beschränkt: entweder ist es syntaktisch verboten im Ausdrucksform, oder es ist mit genau derselben Semantik erlaubt wie in der Anweisungsform (nämlich, dass es BaseException abfängt und es nicht mit ‚as‘ erfassen kann).
Leere except-Klauseln
PEP 8 rät zu Recht von der Verwendung eines leeren ‚except‘ ab. Obwohl es syntaktisch in einer Anweisung zulässig ist und aus Gründen der Abwärtskompatibilität so bleiben muss, gibt es wenig Wert darin, seine Verwendung zu fördern. In einer Expression-Except-Klausel ist „except:“ ein SyntaxError; verwenden Sie stattdessen die langform „except BaseException:“ anstelle. Eine zukünftige Version von Python KANN wählen, dies wieder einzuführen, was ohne Bruch der Kompatibilität geschehen kann.
Klammern um die except-Klauseln
Sollte es zulässig sein, die except-Klauseln zu parenthesieren, getrennt von dem Ausdruck, der auslösen könnte? Beispiel
value = expr (
except Exception1 [as e]: default1
except Exception2 [as e]: default2
# ... except ExceptionN [as e]: defaultN
)
Dies ist überzeugender, wenn eine oder beide der zurückgestellten Untervorschläge von mehreren except-Klauseln und/oder Ausnahmeerfassung enthalten sind. In deren Abwesenheit wären die Klammern somit
value = expr except ExceptionType: default
value = expr (except ExceptionType: default)
Der Vorteil ist minimal, und das Potenzial, einen Leser zu verwirren, indem man denkt, die except-Klausel sei vom Ausdruck getrennt, oder dass dies ein Funktionsaufruf sei, macht dies nicht überzeugend. Der Ausdruck kann natürlich, wenn gewünscht, parenthesiert werden, ebenso wie der Standardwert
value = (expr) except ExceptionType: (default)
Da der gesamte Ausdruck nun in Klammern stehen muss (was zum Zeitpunkt der Debatte noch nicht entschieden war), besteht weniger Bedarf, diesen Abschnitt abzugrenzen, und in vielen Fällen wäre er redundant.
Kurzform für „except: pass“
Das Folgende wurde als ähnliche Kurzform vorgeschlagen, wenn auch technisch kein Ausdruck
statement except Exception: pass
try:
statement
except Exception:
pass
Zum Beispiel ist ein häufiger Anwendungsfall das Löschen einer Datei
os.unlink(some_file) except OSError: pass
Es gibt bereits ein Äquivalent in Python 3.4, jedoch in contextlib
from contextlib import suppress
with suppress(OSError): os.unlink(some_file)
Da dies bereits eine einzelne Zeile ist (oder zwei mit einer Zeilenunterbrechung nach dem Doppelpunkt), gibt es wenig Bedarf an neuer Syntax und Verwirrung zwischen Anweisung und Ausdruck, um dies zu erreichen.
Häufige Einwände
Doppelpunkte leiten immer Suiten ein
Während es stimmt, dass viele syntaktische Elemente von Python den Doppelpunkt verwenden, um eine Anweisungssuite einzuleiten (if, while, with, for, etc.), ist dies keineswegs der einzige Verwendungszweck des Doppelpunkts. Derzeit umfasst die Python-Syntax vier Fälle, in denen ein Doppelpunkt einen Unterausdruck einleitet
- Dictionary-Anzeige – { … Schlüssel:Wert … }
- Slice-Notation – [Start:Stop:Schritt]
- Funktionsdefinition – Parameter: Annotation
- Lambda – Argumentliste: Rückgabewert
Dieser Vorschlag fügt einfach eine fünfte hinzu
- Except-Ausdruck – Ausnahmeliste: Ergebnis
Styleguides und PEP 8 empfehlen zu Recht, den Doppelpunkt nicht am Ende einer umgebrochenen Zeile zu haben, die potenziell wie die Einleitung einer Suite aussehen könnte, sondern stattdessen das Umbrechen vor der Ausnahmeliste zu befürworten, wobei der Doppelpunkt klar zwischen zwei Ausdrücken bleibt.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0463.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT