PEP 654 – Exception Groups und except*
- Autor:
- Irit Katriel <irit at python.org>, Yury Selivanov <yury at edgedb.com>, Guido van Rossum <guido at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 22-Feb-2021
- Python-Version:
- 3.11
- Post-History:
- 22-Feb-2021, 20-Mar-2021, 03-Oct-2021
- Resolution:
- Discourse-Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Begründung
- Spezifikation
- Abwärtskompatibilität
- Wie man das lehrt
- Referenzimplementierung
- Abgelehnte Ideen
- Exception Groups iterierbar machen
ExceptionGroupvonBaseExceptionerben lassen- Verhindern, dass
BaseExceptionsin einer Exception Group verpackt werden - Traceback-Darstellung
excepterweitern, um Exception Groups zu behandeln- Eine neue
except-Alternative - Eine
except*-Klausel auf eine Exception gleichzeitig anwenden - Keine Übereinstimmung mit nackten Exceptions in
except* - Mischen von
except:undexcept*:im selbentryerlauben try*stattexcept*- Alternative Syntaxoptionen
- Programmierung ohne „except *“
- Siehe auch
- Danksagungen
- Akzeptanz
- Referenzen
- Urheberrecht
Zusammenfassung
Dieses Dokument schlägt Spracherweiterungen vor, die es Programmen ermöglichen, mehrere unabhängige Exceptions gleichzeitig auszulösen und zu behandeln
- Ein neuer Standard-Exception-Typ, die
ExceptionGroup, die eine Gruppe von unabhängigen Exceptions darstellt, die gemeinsam propagiert werden. - Eine neue Syntax
except*zur Behandlung vonExceptionGroups.
Motivation
Der Interpreter kann derzeit höchstens eine Exception gleichzeitig propagieren. Die in PEP 3134 eingeführten Verkettungsfunktionen verknüpfen Exceptions, die sich gegenseitig als Ursache oder Kontext haben, aber es gibt Situationen, in denen mehrere unabhängige Exceptions gemeinsam propagiert werden müssen, wenn der Stack entrollt wird. Mehrere reale Anwendungsfälle werden unten aufgeführt.
- Gleichzeitige Fehler. Bibliotheken für asynchrone Nebenläufigkeit bieten APIs zum Aufrufen mehrerer Aufgaben und zur aggregierten Rückgabe ihrer Ergebnisse. Derzeit gibt es keine gute Möglichkeit für solche Bibliotheken, Situationen zu behandeln, in denen mehrere Aufgaben Exceptions auslösen. Die Funktion
asyncio.gather()[1] der Python-Standardbibliothek bietet zwei Optionen: die erste Exception auslösen oder die Exceptions in der Ergebnisliste zurückgeben. Die Trio-Bibliothek [2] verfügt über einen Exception-TypMultiError, den sie zur Meldung einer Sammlung von Fehlern auslöst. Die Arbeit an diesem PEP wurde ursprünglich durch die Schwierigkeiten bei der Behandlung vonMultiErrors[9] motiviert, die in einem Design-Dokument für eine verbesserte Version,MultiError2[3], detailliert sind. Dieses Dokument zeigt, wie schwierig es ist, eine effektive API zur Meldung und Behandlung mehrerer Fehler ohne die von uns vorgeschlagenen Sprachänderungen zu erstellen (siehe auch den Abschnitt Programmierung ohne ‚except *‘).Die Implementierung einer besseren Task-Spawn-API in asyncio, inspiriert von Trio-Nurseries [13], war die Hauptmotivation für dieses PEP. Diese Arbeit wird derzeit durch das Fehlen nativer Sprachunterstützung für Exception Groups in Python blockiert.
- Mehrere Fehler beim erneuten Versuchen einer Operation. Die Funktion
socket.create_connectionder Python-Standardbibliothek kann versuchen, eine Verbindung zu verschiedenen Adressen herzustellen. Wenn alle Versuche fehlschlagen, muss dies dem Benutzer gemeldet werden. Es ist eine offene Frage, wie diese Fehler aggregiert werden sollen, insbesondere wenn sie unterschiedlich sind (siehe Issue 29980 [4]). - Mehrere Benutzer-Callbacks schlagen fehl. Die Funktion
atexit.register()von Python ermöglicht es Benutzern, Funktionen zu registrieren, die beim Systemende aufgerufen werden. Wenn eine davon Exceptions auslöst, wird nur die letzte erneut ausgelöst, aber es wäre besser, alle gemeinsam erneut auszulösen (siehe Dokumentation zuatexit[5]). Ähnlich erlaubt die pytest-Bibliothek Benutzern, Finalizer zu registrieren, die beim Aufräumen ausgeführt werden. Wenn mehr als einer dieser Finalizer eine Exception auslöst, wird nur die erste dem Benutzer gemeldet. Dies kann mitExceptionGroupsverbessert werden, wie vom pytest-Entwickler Ran Benita in diesem Issue erläutert (siehe pytest issue 8217 [6]). - Mehrere Fehler bei einer komplexen Berechnung. Die Hypothesis-Bibliothek führt eine automatische Fehlerreduktion durch (Vereinfachung von Code, der einen Fehler demonstriert). Dabei können Variationen gefunden werden, die unterschiedliche Fehler generieren, und (optional) werden alle gemeldet (siehe Dokumentation zu Hypothesis [7]). Ein
ExceptionGroup-Mechanismus, wie wir ihn hier vorschlagen, kann einige der Schwierigkeiten bei der Fehlersuche lösen, die in dem obigen Link erwähnt werden und die auf den Verlust von Kontext-/Ursacheninformationen zurückzuführen sind (kommuniziert von Hypothesis Core Developer Zac Hatfield-Dodds). - Fehler im Wrapper-Code. Der Kontextmanager
tempfile.TemporaryDirectoryder Python-Standardbibliothek hatte ein Problem, bei dem eine im__exit__während der Bereinigung ausgelöste Exception effektiv eine vom Benutzercode innerhalb des Kontextmanager-Scopes ausgelöste Exception maskierte. Während die Benutzerexception als Kontext des Bereinigungsfehlers verkettet wurde, wurde sie nicht von der Benutzerexcept-Klausel abgefangen (siehe Issue 40857 [8]).Das Problem wurde behoben, indem der Bereinigungscode Fehler ignorierte und so das Problem mehrerer Exceptions umging. Mit den hier vorgeschlagenen Funktionen wäre es möglich, dass
__exit__eineExceptionGroupauslöst, die ihre eigenen Fehler zusammen mit den Fehlern des Benutzers enthält, und dies würde es dem Benutzer ermöglichen, seine eigenen Exceptions nach ihrem Typ abzufangen.
Begründung
Mehrere Exceptions zu gruppieren, kann ohne Änderungen an der Sprache erfolgen, indem einfach ein Container-Exception-Typ erstellt wird. Trio [2] ist ein Beispiel für eine Bibliothek, die diese Technik in ihrem MultiError [9]-Typ verwendet hat. Ein solcher Ansatz erfordert jedoch, dass der aufrufende Code den Container-Exception-Typ abfängt und ihn dann inspiziert, um die Typen der aufgetretenen Fehler zu ermitteln, die gewünschten zu extrahieren und die restlichen erneut auszulösen. Darüber hinaus haben Exceptions in Python wichtige Informationen, die an ihre Felder __traceback__, __cause__ und __context__ angehängt sind, und das Entwerfen eines Containertyps, der die Integrität dieser Informationen bewahrt, erfordert Sorgfalt; es ist nicht so einfach, wie Exceptions in einer Menge zu sammeln.
Es sind Sprachänderungen erforderlich, um die Unterstützung für Exception Groups im Stil bestehender Exception-Handling-Mechanismen zu erweitern. Zumindest möchten wir eine Exception Group nur abfangen können, wenn sie eine Exception eines Typs enthält, den wir zu behandeln wählen. Exceptions anderer Typen in derselben Gruppe müssen automatisch erneut ausgelöst werden, da es sonst zu einfach ist, dass Benutzercode versehentlich Exceptions verschluckt, die er nicht behandelt.
Wir haben geprüft, ob es möglich ist, die Semantik von except zu diesem Zweck rückwärtskompatibel zu ändern, und festgestellt, dass dies nicht möglich ist. Siehe den Abschnitt Abgelehnte Ideen für weitere Informationen.
Der Zweck dieses PEP ist es daher, den eingebauten Typ ExceptionGroup und die except*-Syntax zur Behandlung von Exception Groups im Interpreter hinzuzufügen. Die gewünschte Semantik von except* unterscheidet sich ausreichend von der aktuellen Exception-Handling-Semantik, dass wir nicht vorschlagen, das Verhalten des except-Schlüsselworts zu ändern, sondern stattdessen die neue except*-Syntax hinzuzufügen.
Unsere Prämisse ist, dass Exception Groups und except* selektiv, nur bei Bedarf, verwendet werden. Wir erwarten nicht, dass sie zum Standardmechanismus für die Exception-Behandlung werden. Die Entscheidung, Exception Groups aus einer Bibliothek auszulösen, muss sorgfältig abgewogen und als API-breaking change betrachtet werden. Wir erwarten, dass dies normalerweise durch Einführung einer neuen API und nicht durch Änderung einer bestehenden erfolgt.
Spezifikation
ExceptionGroup und BaseExceptionGroup
Wir schlagen vor, zwei neue eingebaute Exception-Typen hinzuzufügen: BaseExceptionGroup(BaseException) und ExceptionGroup(BaseExceptionGroup, Exception). Sie sind zu Exception.__cause__ und Exception.__context__ zuweisbar und können wie jede Exception mit raise ExceptionGroup(...) und try: ... except ExceptionGroup: ... oder raise BaseExceptionGroup(...) und try: ... except BaseExceptionGroup: ... ausgelöst und behandelt werden.
Beide haben einen Konstruktor, der zwei positionsgebundene Argumente entgegennimmt: eine Nachrichtenzeichenkette und eine Sequenz der verschachtelten Exceptions, die in den Feldern message und exceptions verfügbar sind. Beispiel: ExceptionGroup('issues', [ValueError('bad value'), TypeError('bad type')]). Der Unterschied zwischen beiden besteht darin, dass ExceptionGroup nur Exception-Unterklassen verpacken kann, während BaseExceptionGroup jede BaseException-Unterklasse verpacken kann. Der Konstruktor von BaseExceptionGroup prüft die verschachtelten Exceptions und gibt eine ExceptionGroup zurück, wenn alle Exceptions Exception-Unterklassen sind, ansonsten eine BaseExceptionGroup. Der Konstruktor von ExceptionGroup löst eine TypeError aus, wenn eine der verschachtelten Exceptions keine Exception-Instanz ist. Im Rest des Dokuments meinen wir mit Exception Group entweder eine ExceptionGroup oder eine BaseExceptionGroup. Wenn die Unterscheidung notwendig ist, verwenden wir den Klassennamen. Zur Vereinfachung verwenden wir ExceptionGroup in Codebeispielen, die für beide relevant sind.
Da eine Exception Group verschachtelt sein kann, repräsentiert sie einen Baum von Exceptions, wobei die Blätter normale Exceptions sind und jeder innere Knoten eine Zeit darstellt, zu der das Programm einige unabhängige Exceptions zu einer neuen Gruppe zusammengefasst und gemeinsam ausgelöst hat.
Die Methode BaseExceptionGroup.subgroup(condition) bietet uns eine Möglichkeit, eine Exception Group zu erhalten, die dieselben Metadaten (Nachricht, Ursache, Kontext, Traceback) wie die ursprüngliche Gruppe hat und dieselbe verschachtelte Struktur von Gruppen, aber nur die Exceptions enthält, für die die Bedingung wahr ist.
>>> eg = ExceptionGroup(
... "one",
... [
... TypeError(1),
... ExceptionGroup(
... "two",
... [TypeError(2), ValueError(3)]
... ),
... ExceptionGroup(
... "three",
... [OSError(4)]
... )
... ]
... )
>>> import traceback
>>> traceback.print_exception(eg)
| ExceptionGroup: one (3 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 1
+---------------- 2 ----------------
| ExceptionGroup: two (2 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 2
+---------------- 2 ----------------
| ValueError: 3
+------------------------------------
+---------------- 3 ----------------
| ExceptionGroup: three (1 sub-exception)
+-+---------------- 1 ----------------
| OSError: 4
+------------------------------------
>>> type_errors = eg.subgroup(lambda e: isinstance(e, TypeError))
>>> traceback.print_exception(type_errors)
| ExceptionGroup: one (2 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 1
+---------------- 2 ----------------
| ExceptionGroup: two (1 sub-exception)
+-+---------------- 1 ----------------
| TypeError: 2
+------------------------------------
>>>
Die Übereinstimmungsbedingung wird auch auf innere Knoten (die Exception Groups) angewendet, und eine Übereinstimmung bewirkt, dass der gesamte Teilbaum, der an diesem Knoten verwurzelt ist, in das Ergebnis aufgenommen wird.
Leere verschachtelte Gruppen werden aus dem Ergebnis weggelassen, wie im Beispiel von ExceptionGroup("three") oben. Wenn keine der Exceptions mit der Bedingung übereinstimmt, gibt subgroup None anstelle einer leeren Gruppe zurück. Die ursprüngliche eg wird durch subgroup nicht verändert, aber der zurückgegebene Wert ist nicht unbedingt eine vollständige neue Kopie. Blatt-Exceptions werden nicht kopiert, ebenso wenig Exception Groups, die vollständig im Ergebnis enthalten sind. Wenn eine Gruppe partitioniert werden muss, weil die Bedingung für einige, aber nicht alle darin enthaltenen Exceptions gilt, wird eine neue Instanz von ExceptionGroup oder BaseExceptionGroup erstellt, während die Felder __cause__, __context__ und __traceback__ per Referenz kopiert werden, sodass sie mit der ursprünglichen eg geteilt werden.
Wenn sowohl die Untergruppe als auch ihr Komplement benötigt werden, kann die Methode BaseExceptionGroup.split(condition) verwendet werden.
>>> type_errors, other_errors = eg.split(lambda e: isinstance(e, TypeError))
>>> traceback.print_exception(type_errors)
| ExceptionGroup: one (2 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 1
+---------------- 2 ----------------
| ExceptionGroup: two (1 sub-exception)
+-+---------------- 1 ----------------
| TypeError: 2
+------------------------------------
>>> traceback.print_exception(other_errors)
| ExceptionGroup: one (2 sub-exceptions)
+-+---------------- 1 ----------------
| ExceptionGroup: two (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: 3
+------------------------------------
+---------------- 2 ----------------
| ExceptionGroup: three (1 sub-exception)
+-+---------------- 1 ----------------
| OSError: 4
+------------------------------------
>>>
Wenn eine Teilung trivial ist (eine Seite ist leer), wird für die andere Seite None zurückgegeben.
>>> other_errors.split(lambda e: isinstance(e, SyntaxError))
(None, ExceptionGroup('one', [
ExceptionGroup('two', [
ValueError(3)
]),
ExceptionGroup('three', [
OSError(4)])]))
Da die Teilung nach Exception-Typ ein sehr häufiger Anwendungsfall ist, können subgroup und split einen Exception-Typ oder ein Tupel von Exception-Typen entgegennehmen und diese als Kurzform für die Übereinstimmung dieses Typs behandeln: eg.split(T) teilt eg in die Untergruppe der Blatt-Exceptions, die mit dem Typ T übereinstimmen, und die Untergruppe derer, die dies nicht tun (unter Verwendung derselben Prüfung wie except für eine Übereinstimmung).
Subklassen von Exception Groups
Es ist möglich, Exception Groups zu unterklassen, aber dabei ist es normalerweise notwendig, anzugeben, wie subgroup() und split() neue Instanzen für den übereinstimmenden oder nicht übereinstimmenden Teil der Partition erstellen sollen. BaseExceptionGroup stellt eine Instanzmethode derive(self, excs) zur Verfügung, die aufgerufen wird, wenn subgroup und split eine neue Exception Group erstellen müssen. Der Parameter excs ist die Sequenz der Exceptions, die in die neue Gruppe aufgenommen werden sollen. Da derive Zugriff auf self hat, kann es Daten von diesem Objekt auf das neue Objekt kopieren. Wenn wir beispielsweise eine Unterklasse von Exception Group benötigen, die ein zusätzliches Fehlercodefeld hat, können wir Folgendes tun:
class MyExceptionGroup(ExceptionGroup):
def __new__(cls, message, excs, errcode):
obj = super().__new__(cls, message, excs)
obj.errcode = errcode
return obj
def derive(self, excs):
return MyExceptionGroup(self.message, excs, self.errcode)
Beachten Sie, dass wir __new__ anstelle von __init__ überschreiben; dies liegt daran, dass BaseExceptionGroup.__new__ die Konstruktorargumente prüfen muss und seine Signatur sich von der der Unterklasse unterscheidet. Beachten Sie auch, dass unsere derive-Funktion die Felder __context__, __cause__ und __traceback__ nicht kopiert, da subgroup und split dies für uns tun.
Mit der oben definierten Klasse haben wir Folgendes:
>>> eg = MyExceptionGroup("eg", [TypeError(1), ValueError(2)], 42)
>>>
>>> match, rest = eg.split(ValueError)
>>> print(f'match: {match!r}: {match.errcode}')
match: MyExceptionGroup('eg', [ValueError(2)], 42): 42
>>> print(f'rest: {rest!r}: {rest.errcode}')
rest: MyExceptionGroup('eg', [TypeError(1)], 42): 42
>>>
Wenn wir derive nicht überschreiben, ruft split die in BaseExceptionGroup definierte auf, die eine Instanz von ExceptionGroup zurückgibt, wenn alle enthaltenen Exceptions vom Typ Exception sind, und andernfalls BaseExceptionGroup. Zum Beispiel:
>>> class MyExceptionGroup(BaseExceptionGroup):
... pass
...
>>> eg = MyExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)])
>>> match, rest = eg.split(ValueError)
>>> print(f'match: {match!r}')
match: ExceptionGroup('eg', [ValueError(1)])
>>> print(f'rest: {rest!r}')
rest: BaseExceptionGroup('eg', [KeyboardInterrupt(2)])
>>>
Der Traceback einer Exception Group
Bei regulären Exceptions stellt der Traceback einen einfachen Pfad von Frames dar, vom Frame, in dem die Exception ausgelöst wurde, bis zu dem Frame, in dem sie abgefangen wurde, oder, falls sie noch nicht abgefangen wurde, dem Frame, in dem sich die Ausführung des Programms gerade befindet. Die Liste wird vom Interpreter erstellt, der jeden Frame, aus dem er austritt, zum Traceback der „aktuellen Exception“ hinzufügt, falls eine vorhanden ist. Um effiziente Anhänge zu unterstützen, sind die Links in der Frame-Liste eines Tracebacks vom ältesten zum neuesten Frame. Das Anhängen eines neuen Frames ist dann einfach eine Frage des Einfügens eines neuen Kopfes in die verkettete Liste, auf die über das Feld __traceback__ der Exception verwiesen wird. Entscheidend ist, dass die Frame-Liste des Tracebacks in dem Sinne unveränderlich ist, dass Frames nur am Kopf hinzugefügt werden müssen und niemals entfernt werden müssen.
Wir müssen keine Änderungen an dieser Datenstruktur vornehmen. Das Feld __traceback__ der Exception-Group-Instanz repräsentiert den Pfad, den die enthaltenen Exceptions gemeinsam zurückgelegt haben, nachdem sie zur Gruppe zusammengefasst wurden, und dasselbe Feld auf jeder der verschachtelten Exceptions repräsentiert den Pfad, über den diese Exception den Frame der Zusammenführung erreicht hat.
Was wir ändern müssen, ist jeder Code, der Tracebacks interpretiert und anzeigt, denn er muss nun in die Tracebacks von verschachtelten Exceptions fortfahren, wie im folgenden Beispiel:
>>> def f(v):
... try:
... raise ValueError(v)
... except ValueError as e:
... return e
...
>>> try:
... raise ExceptionGroup("one", [f(1)])
... except ExceptionGroup as e:
... eg = e
...
>>> raise ExceptionGroup("two", [f(2), eg])
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: two (2 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in f
| ValueError: 2
+---------------- 2 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: one (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in f
| ValueError: 1
+------------------------------------
>>>
Exception Groups behandeln
Wir gehen davon aus, dass Programme, die Exception Groups abfangen und behandeln, typischerweise entweder abfragen, ob sie Blatt-Exceptions haben, für die eine bestimmte Bedingung gilt (mithilfe von subgroup oder split), oder die Exception formatieren (mithilfe der Methoden des traceback-Moduls).
Es ist weniger nützlich, über die einzelnen Blatt-Exceptions zu iterieren. Um zu sehen, warum: Angenommen, eine Anwendung hat eine Exception Group abgefangen, die von einem asyncio.gather()-Aufruf ausgelöst wurde. Zu diesem Zeitpunkt ist der Kontext für jede spezifische Exception verloren gegangen. Jede Wiederherstellung für diese Exception hätte erfolgen müssen, bevor sie mit anderen Exceptions gruppiert wurde [10]. Außerdem wird die Anwendung wahrscheinlich auf dieselbe Weise auf jede Anzahl von Instanzen eines bestimmten Exception-Typs reagieren, sodass es wahrscheinlicher ist, dass wir wissen wollen, ob eg.subgroup(T) None ist oder nicht, als dass wir uns für die Anzahl der Ts in eg interessieren.
Es gibt jedoch Situationen, in denen es notwendig ist, die einzelnen Blatt-Exceptions zu inspizieren. Angenommen, wir haben eine Exception Group eg und wollen die OSErrors mit einem bestimmten Fehlercode protokollieren und alles andere erneut auslösen. Dies können wir tun, indem wir eine Funktion mit Seiteneffekten an subgroup übergeben, wie folgt:
def log_and_ignore_ENOENT(err):
if isinstance(err, OSError) and err.errno == ENOENT:
log(err)
return False
else:
return True
try:
. . .
except ExceptionGroup as eg:
eg = eg.subgroup(log_and_ignore_ENOENT)
if eg is not None:
raise eg
Im vorherigen Beispiel ist, wenn log_and_ignore_ENOENT auf eine Blatt-Exception aufgerufen wird, nur ein Teil des Tracebacks dieser Exception zugänglich – der Teil, auf den über ihr Feld __traceback__ verwiesen wird. Wenn wir den vollständigen Traceback benötigen, müssen wir die Verkettung der Tracebacks der Exceptions auf dem Pfad von der Wurzel zu diesem Blatt betrachten. Dies erhalten wir durch direkte, rekursive Iteration, wie folgt:
def leaf_generator(exc, tbs=None):
if tbs is None:
tbs = []
tbs.append(exc.__traceback__)
if isinstance(exc, BaseExceptionGroup):
for e in exc.exceptions:
yield from leaf_generator(e, tbs)
else:
# exc is a leaf exception and its traceback
# is the concatenation of the traceback
# segments in tbs.
# Note: the list returned (tbs) is reused in each iteration
# through the generator. Make a copy if your use case holds
# on to it beyond the current iteration or mutates its contents.
yield exc, tbs
tbs.pop()
Wir können dann die vollständigen Tracebacks der Blatt-Exceptions verarbeiten:
>>> import traceback
>>>
>>> def g(v):
... try:
... raise ValueError(v)
... except Exception as e:
... return e
...
>>> def f():
... raise ExceptionGroup("eg", [g(1), g(2)])
...
>>> try:
... f()
... except BaseException as e:
... eg = e
...
>>> for (i, (exc, tbs)) in enumerate(leaf_generator(eg)):
... print(f"\n=== Exception #{i+1}:")
... traceback.print_exception(exc)
... print(f"The complete traceback for Exception #{i+1}:")
... for tb in tbs:
... traceback.print_tb(tb)
...
=== Exception #1:
Traceback (most recent call last):
File "<stdin>", line 3, in g
ValueError: 1
The complete traceback for Exception #1
File "<stdin>", line 2, in <module>
File "<stdin>", line 2, in f
File "<stdin>", line 3, in g
=== Exception #2:
Traceback (most recent call last):
File "<stdin>", line 3, in g
ValueError: 2
The complete traceback for Exception #2:
File "<stdin>", line 2, in <module>
File "<stdin>", line 2, in f
File "<stdin>", line 3, in g
>>>
except*
Wir schlagen vor, eine neue Variante der try..except-Syntax einzuführen, um die Arbeit mit Exception Groups zu vereinfachen. Das Zeichen * zeigt an, dass mehrere Exceptions von jeder except*-Klausel behandelt werden können.
try:
...
except* SpamError:
...
except* FooError as e:
...
except* (BarError, BazError) as e:
...
In einer traditionellen try-except-Anweisung gibt es nur eine Exception zu behandeln, daher wird der Körper von höchstens einer except-Klausel ausgeführt; die erste, die mit der Exception übereinstimmt. Mit der neuen Syntax kann eine except*-Klausel mit einer Untergruppe der ausgelösten Exception-Group übereinstimmen, während der verbleibende Teil von nachfolgenden except*-Klauseln übereingestimmt wird. Mit anderen Worten, eine einzelne Exception Group kann dazu führen, dass mehrere except*-Klauseln ausgeführt werden, aber jede solche Klausel wird höchstens einmal ausgeführt (für alle übereinstimmenden Exceptions aus der Gruppe) und jede Exception wird entweder von genau einer Klausel behandelt (der ersten, die ihrem Typ entspricht) oder am Ende erneut ausgelöst. Die Art und Weise, wie jede Exception von einem try-except*-Block behandelt wird, ist unabhängig von anderen Exceptions in der Gruppe.
Angenommen beispielsweise, der Körper des obigen try-Blocks löst eg = ExceptionGroup('msg', [FooError(1), FooError(2), BazError()]) aus. Die except*-Klauseln werden in Reihenfolge ausgewertet, indem split auf die unhandled Exception Group angewendet wird, die anfangs gleich eg ist und dann schrumpft, während Exceptions übereinstimmen und daraus extrahiert werden. In der ersten except*-Klausel gibt unhandled.split(SpamError) (None, unhandled) zurück, sodass der Körper dieses Blocks nicht ausgeführt wird und unhandled unverändert bleibt. Für den zweiten Block gibt unhandled.split(FooError) eine nicht-triviale Teilung (match, rest) zurück, mit match = ExceptionGroup('msg', [FooError(1), FooError(2)]) und rest = ExceptionGroup('msg', [BazError()]). Der Körper dieses except*-Blocks wird ausgeführt, wobei der Wert von e und sys.exc_info() auf match gesetzt wird. Dann wird unhandled auf rest gesetzt. Schließlich stimmt der dritte Block mit der verbleibenden Exception überein, sodass er mit e und sys.exc_info() gleich ExceptionGroup('msg', [BazError()]) ausgeführt wird.
Exceptions werden mithilfe einer Unterklassenprüfung abgeglichen. Zum Beispiel:
try:
low_level_os_operation()
except* OSError as eg:
for e in eg.exceptions:
print(type(e).__name__)
könnte folgendes ausgeben:
BlockingIOError
ConnectionRefusedError
OSError
InterruptedError
BlockingIOError
Die Reihenfolge der except*-Klauseln ist wichtig, genau wie bei der regulären try..except-Anweisung.
>>> try:
... raise ExceptionGroup("problem", [BlockingIOError()])
... except* OSError as e: # Would catch the error
... print(repr(e))
... except* BlockingIOError: # Would never run
... print('never')
...
ExceptionGroup('problem', [BlockingIOError()])
Rekursive Übereinstimmung
Die Übereinstimmung von except*-Klauseln mit einer Exception Group erfolgt rekursiv unter Verwendung der Methode split().
>>> try:
... raise ExceptionGroup(
... "eg",
... [
... ValueError('a'),
... TypeError('b'),
... ExceptionGroup(
... "nested",
... [TypeError('c'), KeyError('d')])
... ]
... )
... except* TypeError as e1:
... print(f'e1 = {e1!r}')
... except* Exception as e2:
... print(f'e2 = {e2!r}')
...
e1 = ExceptionGroup('eg', [TypeError('b'), ExceptionGroup('nested', [TypeError('c')])])
e2 = ExceptionGroup('eg', [ValueError('a'), ExceptionGroup('nested', [KeyError('d')])])
>>>
Nicht übereinstimmende Exceptions
Wenn nicht alle Exceptions in einer Exception Group von den except*-Klauseln abgeglichen wurden, wird der verbleibende Teil der Gruppe weiter propagiert.
>>> try:
... try:
... raise ExceptionGroup(
... "msg", [
... ValueError('a'), TypeError('b'),
... TypeError('c'), KeyError('e')
... ]
... )
... except* ValueError as e:
... print(f'got some ValueErrors: {e!r}')
... except* TypeError as e:
... print(f'got some TypeErrors: {e!r}')
... except ExceptionGroup as e:
... print(f'propagated: {e!r}')
...
got some ValueErrors: ExceptionGroup('msg', [ValueError('a')])
got some TypeErrors: ExceptionGroup('msg', [TypeError('b'), TypeError('c')])
propagated: ExceptionGroup('msg', [KeyError('e')])
>>>
Nackte Exceptions
Wenn die Ausnahme, die im try-Block ausgelöst wird, nicht vom Typ ExceptionGroup oder BaseExceptionGroup ist, nennen wir sie eine naked-Ausnahme. Wenn ihr Typ mit einer der except*-Klauseln übereinstimmt, wird sie abgefangen und von einer ExceptionGroup (oder BaseExceptionGroup, wenn es sich nicht um eine Exception-Unterklasse handelt) mit einer leeren Nachrichtenzeichenfolge umschlossen. Dies dient dazu, den Typ von e konsistent und statisch bekannt zu machen.
>>> try:
... raise BlockingIOError
... except* OSError as e:
... print(repr(e))
...
ExceptionGroup('', [BlockingIOError()])
Wenn jedoch eine naked-Ausnahme nicht abgefangen wird, breitet sie sich in ihrer ursprünglichen naked-Form aus.
>>> try:
... try:
... raise ValueError(12)
... except* TypeError as e:
... print('never')
... except ValueError as e:
... print(f'caught ValueError: {e!r}')
...
caught ValueError: ValueError(12)
>>>
Exceptions in einem except*-Block auslösen
In einem traditionellen except-Block gibt es zwei Möglichkeiten, Ausnahmen auszulösen: raise e, um explizit ein Ausnahmeobjekt e auszulösen, oder naked raise, um die „aktuelle Ausnahme“ erneut auszulösen. Wenn e die aktuelle Ausnahme ist, sind die beiden Formen nicht äquivalent, da ein erneutes Auslösen den aktuellen Frame nicht zum Stack hinzufügt.
def foo(): | def foo():
try: | try:
1 / 0 | 1 / 0
except ZeroDivisionError as e: | except ZeroDivisionError:
raise e | raise
|
foo() | foo()
|
Traceback (most recent call last): | Traceback (most recent call last):
File "/Users/guido/a.py", line 7 | File "/Users/guido/b.py", line 7
foo() | foo()
File "/Users/guido/a.py", line 5 | File "/Users/guido/b.py", line 3
raise e | 1/0
File "/Users/guido/a.py", line 3 | ZeroDivisionError: division by zero
1/0 |
ZeroDivisionError: division by zero |
Dies gilt auch für Ausnahme-Gruppen, aber die Situation ist nun komplexer, da Ausnahmen aus mehreren except*-Klauseln ausgelöst und erneut ausgelöst werden können, sowie unbehandelte Ausnahmen, die sich ausbreiten müssen. Der Interpreter muss all diese Ausnahmen zu einem Ergebnis kombinieren und dieses auslösen.
Die erneut ausgelösten Ausnahmen und die unbehandelten Ausnahmen sind Untergruppen der ursprünglichen Gruppe und teilen deren Metadaten (Ursache, Kontext, Traceback). Andererseits hat jede der explizit ausgelösten Ausnahmen ihre eigenen Metadaten – der Traceback enthält die Zeile, von der aus sie ausgelöst wurde, ihre Ursache ist alles, wozu sie explizit verkettet wurde, und ihr Kontext ist der Wert von sys.exc_info() in der except*-Klausel des Raises.
In der aggregierten Ausnahme-Gruppe haben die erneut ausgelösten und unbehandelten Ausnahmen dieselbe relative Struktur wie in der ursprünglichen Ausnahme, als ob sie gemeinsam in einer subgroup-Aufruf aufgeteilt worden wären. Zum Beispiel löst der innere try-except*-Block im folgenden Ausschnitt eine ExceptionGroup aus, die alle ValueErrors und TypeErrors enthält, die wieder in die gleiche Form zusammengeführt wurden, die sie in der ursprünglichen ExceptionGroup hatten.
>>> try:
... try:
... raise ExceptionGroup(
... "eg",
... [
... ValueError(1),
... TypeError(2),
... OSError(3),
... ExceptionGroup(
... "nested",
... [OSError(4), TypeError(5), ValueError(6)])
... ]
... )
... except* ValueError as e:
... print(f'*ValueError: {e!r}')
... raise
... except* OSError as e:
... print(f'*OSError: {e!r}')
... except ExceptionGroup as e:
... print(repr(e))
...
*ValueError: ExceptionGroup('eg', [ValueError(1), ExceptionGroup('nested', [ValueError(6)])])
*OSError: ExceptionGroup('eg', [OSError(3), ExceptionGroup('nested', [OSError(4)])])
ExceptionGroup('eg', [ValueError(1), TypeError(2), ExceptionGroup('nested', [TypeError(5), ValueError(6)])])
>>>
Wenn Ausnahmen explizit ausgelöst werden, sind sie unabhängig von der ursprünglichen Ausnahme-Gruppe und können nicht mit ihr zusammengeführt werden (sie haben ihre eigene Ursache, ihren eigenen Kontext und ihren eigenen Traceback). Stattdessen werden sie zu einer neuen ExceptionGroup (oder BaseExceptionGroup) kombiniert, die auch die oben beschriebene Untergruppe der erneut ausgelösten/unbehandelten Ausnahmen enthält.
Im folgenden Beispiel wurden die ValueErrors ausgelöst, sodass sie in ihrer eigenen ExceptionGroup sind, während die OSErrors erneut ausgelöst wurden und daher mit den unbehandelten TypeErrors zusammengeführt wurden.
>>> try:
... raise ExceptionGroup(
... "eg",
... [
... ValueError(1),
... TypeError(2),
... OSError(3),
... ExceptionGroup(
... "nested",
... [OSError(4), TypeError(5), ValueError(6)])
... ]
... )
... except* ValueError as e:
... print(f'*ValueError: {e!r}')
... raise e
... except* OSError as e:
... print(f'*OSError: {e!r}')
... raise
...
*ValueError: ExceptionGroup('eg', [ValueError(1), ExceptionGroup('nested', [ValueError(6)])])
*OSError: ExceptionGroup('eg', [OSError(3), ExceptionGroup('nested', [OSError(4)])])
| ExceptionGroup: (2 sub-exceptions)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 15, in <module>
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg (2 sub-exceptions)
+-+---------------- 1 ----------------
| ValueError: 1
+---------------- 2 ----------------
| ExceptionGroup: nested (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: 6
+------------------------------------
+---------------- 2 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg (3 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 2
+---------------- 2 ----------------
| OSError: 3
+---------------- 3 ----------------
| ExceptionGroup: nested (2 sub-exceptions)
+-+---------------- 1 ----------------
| OSError: 4
+---------------- 2 ----------------
| TypeError: 5
+------------------------------------
>>>
Verkettung
Explizit ausgelöste Ausnahme-Gruppen werden wie jede andere Ausnahme verkettet. Das folgende Beispiel zeigt, wie ein Teil von ExceptionGroup „one“ zum Kontext für ExceptionGroup „two“ wurde, während der andere Teil mit ihr zu der neuen ExceptionGroup kombiniert wurde.
>>> try:
... raise ExceptionGroup("one", [ValueError('a'), TypeError('b')])
... except* ValueError:
... raise ExceptionGroup("two", [KeyError('x'), KeyError('y')])
...
| ExceptionGroup: (2 sub-exceptions)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: one (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: a
+------------------------------------
|
| During handling of the above exception, another exception occurred:
|
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 4, in <module>
| ExceptionGroup: two (2 sub-exceptions)
+-+---------------- 1 ----------------
| KeyError: 'x'
+---------------- 2 ----------------
| KeyError: 'y'
+------------------------------------
+---------------- 2 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: one (1 sub-exception)
+-+---------------- 1 ----------------
| TypeError: b
+------------------------------------
>>>
Neue Exceptions auslösen
In den vorherigen Beispielen waren die expliziten Raises von den abgefangenen Ausnahmen, sodass wir zur Vervollständigung eine neue Ausnahme mit Verkettung zeigen.
>>> try:
... raise TypeError('bad type')
... except* TypeError as e:
... raise ValueError('bad value') from e
...
| ExceptionGroup: (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| TypeError: bad type
+------------------------------------
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ValueError: bad value
>>>
Beachten Sie, dass Ausnahmen, die in einer except*-Klausel ausgelöst werden, nicht berechtigt sind, mit anderen Klauseln desselben try-Statements übereinzustimmen.
>>> try:
... raise TypeError(1)
... except* TypeError:
... raise ValueError(2) from None # <- not caught in the next clause
... except* ValueError:
... print('never')
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ValueError: 2
>>>
Das Auslösen einer neuen Instanz einer naked-Ausnahme führt nicht dazu, dass diese Ausnahme von einer Ausnahme-Gruppe umschlossen wird. Stattdessen wird die Ausnahme wie sie ist ausgelöst, und wenn sie mit anderen weitergegebenen Ausnahmen kombiniert werden muss, wird sie ein direkter Nachkomme der neuen Ausnahme-Gruppe, die dafür erstellt wird.
>>> try:
... raise ExceptionGroup("eg", [ValueError('a')])
... except* ValueError:
... raise KeyError('x')
...
| ExceptionGroup: (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: a
+------------------------------------
|
| During handling of the above exception, another exception occurred:
|
| Traceback (most recent call last):
| File "<stdin>", line 4, in <module>
| KeyError: 'x'
+------------------------------------
>>>
>>> try:
... raise ExceptionGroup("eg", [ValueError('a'), TypeError('b')])
... except* ValueError:
... raise KeyError('x')
...
| ExceptionGroup: (2 sub-exceptions)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg (1 sub-exception)
+-+---------------- 1 ----------------
| ValueError: a
+------------------------------------
|
| During handling of the above exception, another exception occurred:
|
| Traceback (most recent call last):
| File "<stdin>", line 4, in <module>
| KeyError: 'x'
+---------------- 2 ----------------
| Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg (1 sub-exception)
+-+---------------- 1 ----------------
| TypeError: b
+------------------------------------
>>>
Schließlich zeigt der folgende Code als Beispiel dafür, wie die vorgeschlagenen Semantiken uns helfen können, effektiv mit Ausnahme-Gruppen zu arbeiten, dass alle EPIPE-Betriebssystemfehler ignoriert werden, während alle anderen Ausnahmen weitergegeben werden.
try:
low_level_os_operation()
except* OSError as errors:
exc = errors.subgroup(lambda e: e.errno != errno.EPIPE)
if exc is not None:
raise exc from None
Aufgefangene Exception-Objekte
Es ist wichtig hervorzuheben, dass die Ausnahme-Gruppe, die in einer except*-Klausel an e gebunden ist, ein ephemeres Objekt ist. Das Auslösen über raise oder raise e führt nicht zu Änderungen an der Gesamtstruktur der ursprünglichen Ausnahme-Gruppe. Jegliche Modifikationen an e gehen wahrscheinlich verloren.
>>> eg = ExceptionGroup("eg", [TypeError(12)])
>>> eg.foo = 'foo'
>>> try:
... raise eg
... except* TypeError as e:
... e.foo = 'bar'
... # ^----------- ``e`` is an ephemeral object that might get
>>> # destroyed after the ``except*`` clause.
>>> eg.foo
'foo'
Verbotene Kombinationen
Es ist nicht möglich, sowohl traditionelle except-Blöcke als auch die neuen except*-Klauseln im selben try-Statement zu verwenden. Das Folgende ist ein SyntaxError.
try:
...
except ValueError:
pass
except* CancelledError: # <- SyntaxError:
pass # combining ``except`` and ``except*``
# is prohibited
Es ist möglich, die Ausnahmetypen ExceptionGroup und BaseExceptionGroup mit except abzufangen, aber nicht mit except*, da letzteres mehrdeutig ist.
try:
...
except ExceptionGroup: # <- This works
pass
try:
...
except* ExceptionGroup: # <- Runtime error
pass
try:
...
except* (TypeError, ExceptionGroup): # <- Runtime error
pass
Ein leerer except*-Block, der „alles abgleicht“, wird nicht unterstützt, da seine Bedeutung verwirrend sein könnte.
try:
...
except*: # <- SyntaxError
pass
continue, break und return sind in except*-Klauseln nicht zulässig und führen zu einem SyntaxError. Dies liegt daran, dass die Ausnahmen in einer ExceptionGroup als unabhängig voneinander angenommen werden und das Vorhandensein oder Fehlen einer von ihnen die Behandlung der anderen nicht beeinflussen sollte, wie es passieren könnte, wenn wir einer except*-Klausel erlauben würden, den Kontrollfluss durch andere Klauseln zu ändern.
Abwärtskompatibilität
Abwärtskompatibilität war eine Anforderung unseres Designs, und die Änderungen, die wir in diesem PEP vorschlagen, werden keinen vorhandenen Code brechen.
- Die Hinzufügung der neuen eingebauten Ausnahmetypen
ExceptionGroupundBaseExceptionGroupbeeinträchtigt bestehende Programme nicht. Die Art und Weise, wie bestehende Ausnahmen behandelt und angezeigt werden, ändert sich in keiner Weise. - Das Verhalten von
exceptbleibt unverändert, sodass bestehender Code weiterhin funktioniert. Programme werden von den in diesem PEP vorgeschlagenen Änderungen nur dann betroffen sein, wenn sie beginnen, Ausnahme-Gruppen undexcept*zu verwenden. - Ein wichtiges Anliegen war, dass
except Exception:weiterhin fast alle Ausnahmen abfangen wird, und indem wirExceptionGroupvonExceptionerben lassen, haben wir sichergestellt, dass dies der Fall sein wird.BaseExceptionGroupswerden nicht abgefangen, was angemessen ist, da sie Ausnahmen enthalten, die nicht vonexcept Exceptionabgefangen worden wären.
Sobald Programme beginnen, diese Funktionen zu nutzen, müssen Migrationsprobleme berücksichtigt werden.
- Eine
except T:-Klausel, die Code umschließt, der jetzt potenziell eine Ausnahme-Gruppe auslöst, muss möglicherweise zuexcept* T:werden, und ihr Körper muss möglicherweise aktualisiert werden. Das bedeutet, dass das Auslösen einer Ausnahme-Gruppe eine API-breaking-Änderung ist und wahrscheinlich in neuen APIs vorgenommen wird, anstatt zu bestehenden hinzugefügt zu werden. - Bibliotheken, die ältere Python-Versionen unterstützen müssen, können
except*nicht verwenden oder Ausnahme-Gruppen auslösen.
Wie man das lehrt
Ausnahme-Gruppen und except* werden als Teil des Sprachstandards dokumentiert. Bibliotheken, die Ausnahme-Gruppen auslösen, wie asyncio, müssen dies in ihrer Dokumentation angeben und klären, welche API-Aufrufe mit try-except* und nicht mit try-except umschlossen werden müssen.
Referenzimplementierung
Wir haben diese Konzepte (und die Beispiele für diesen PEP) mit Hilfe der Referenzimplementierung entwickelt [11].
Sie enthält die eingebaute ExceptionGroup zusammen mit den Änderungen am Traceback-Formatierungscode, zusätzlich zu den grammatikalischen, Compiler- und Interpreter-Änderungen, die zur Unterstützung von except* erforderlich sind. BaseExceptionGroup wird bald hinzugefügt.
Zwei Opcodes wurden hinzugefügt: einer implementiert die Ausnahme-Typen-Übereinstimmungsprüfung über ExceptionGroup.split(), und der andere wird am Ende einer try-except-Konstruktion verwendet, um alle unbehandelten, ausgelösten und erneut ausgelösten Ausnahmen (falls vorhanden) zusammenzuführen. Die ausgelösten/erneut ausgelösten Ausnahmen werden in einer Liste auf dem Laufzeit-Stack gesammelt. Zu diesem Zweck wird der Körper jeder except*-Klausel in eine traditionelle try-except eingeschlossen, die alle ausgelösten Ausnahmen abfängt. Sowohl ausgelöste als auch erneut ausgelöste Ausnahmen werden in derselben Liste gesammelt. Wenn es an der Zeit ist, sie zu einem Ergebnis zusammenzuführen, werden die ausgelösten und erneut ausgelösten Ausnahmen durch den Vergleich ihrer Metadatenfelder (Kontext, Ursache, Traceback) mit denen der ursprünglich ausgelösten Ausnahme unterschieden. Wie oben erwähnt, haben die erneut ausgelösten Ausnahmen dieselben Metadaten wie das Original, während die ausgelösten dies nicht tun.
Abgelehnte Ideen
Exception Groups iterierbar machen
Wir haben erwogen, Ausnahme-Gruppen iterierbar zu machen, so dass list(eg) eine abgeflachte Liste der Blatt-Ausnahmen erzeugt, die in der Gruppe enthalten sind. Wir entschieden, dass dies keine solide API wäre, da die Metadaten (Ursache, Kontext und Traceback) der einzelnen Ausnahmen in einer Gruppe unvollständig sind und dies Probleme verursachen könnte.
Darüber hinaus, wie wir im Abschnitt Handling Exception Groups erklärt haben, halten wir es für unwahrscheinlich, dass die Iteration über Blatt-Ausnahmen viele Anwendungsfälle haben wird. Wir haben dort jedoch den Code für einen Traversierungsalgorithmus bereitgestellt, der die Metadaten jeder Blatt-Ausnahme korrekt konstruiert. Wenn sich dies in der Praxis als nützlich erweist, können wir diese Funktionalität zukünftig zur Standardbibliothek hinzufügen oder Ausnahme-Gruppen sogar iterierbar machen.
ExceptionGroup von BaseException erben lassen
Wir erwogen, ExceptionGroup nur von BaseException und nicht von Exception erben zu lassen. Die Begründung dafür war, dass wir erwarten, dass Ausnahme-Gruppen auf bewusste Weise verwendet werden, wo sie benötigt werden, und nur von APIs ausgelöst werden, die speziell dafür konzipiert und dokumentiert sind. In diesem Kontext ist eine ExceptionGroup, die aus einer API entkommt, die sie nicht auslösen soll, ein Fehler, und wir wollten ihr den Status „fatal error“ geben, damit except Exception sie nicht versehentlich verschluckt. Dies wäre mit der Art und Weise vereinbar, wie except T: keine Ausnahme-Gruppen abfängt, die T für alle anderen Typen enthalten, und würde helfen, ExceptionGroups auf die Teile des Programms zu beschränken, in denen sie erscheinen sollen. Es war jedoch aus der öffentlichen Diskussion klar, dass T=Exception ein Sonderfall ist, und es gibt Entwickler, die der Meinung sind, dass except Exception: „fast alles“ abfangen sollte, einschließlich Ausnahme-Gruppen. Deshalb haben wir beschlossen, ExceptionGroup zu einer Unterklasse von Exception zu machen.
Verhindern, dass BaseExceptions in einer Exception Group verpackt werden
Eine Folge der Entscheidung, ExceptionGroup von Exception erben zu lassen, ist, dass ExceptionGroup keine BaseExceptions wie KeyboardInterrupt umschließen sollte, da diese derzeit nicht von except Exception: abgefangen werden. Wir haben die Option in Betracht gezogen, es einfach unmöglich zu machen, BaseExceptions zu umschließen, haben uns aber schließlich entschieden, es durch den Typ BaseExceptionGroup zu ermöglichen, der von BaseException und nicht von Exception erbt. Dies zu ermöglichen, verleiht der Sprache Flexibilität und überlässt es dem Programmierer abzuwägen, welchen Vorteil das Umschließen von BaseExceptions gegenüber ihrer naked-Form und dem Verwerfen aller anderen Ausnahmen hat.
Traceback-Darstellung
Wir haben Optionen zur Anpassung der Traceback-Datenstruktur zur Darstellung von Bäumen in Betracht gezogen, aber es wurde offensichtlich, dass ein Traceback-Baum nicht sinnvoll ist, sobald er von den Ausnahmen getrennt ist, auf die er sich bezieht. Während ein Traceback mit einfachem Pfad mit einem with_traceback()-Aufruf an jede Ausnahme angehängt werden kann, ist es schwer vorstellbar, dass es sinnvoll wäre, einem Ausnahme-Gruppen-Traceback-Baum zuzuweisen. Darüber hinaus enthält eine nützliche Anzeige des Tracebacks Informationen über die verschachtelten Ausnahmen. Aus diesen Gründen haben wir beschlossen, den Traceback-Mechanismus so zu belassen, wie er ist, und den Traceback-Anzeigecode zu ändern.
except erweitern, um Exception Groups zu behandeln
Wir haben erwogen, die Semantik von except zu erweitern, um Ausnahme-Gruppen zu behandeln, anstatt except* einzuführen. Es gab zwei Abwärtskompatibilitätsprobleme damit. Das erste ist der Typ der abgefangenen Ausnahme. Betrachten Sie dieses Beispiel:
try:
. . .
except OSError as err:
if err.errno != ENOENT:
raise
Wenn der Wert, der err zugewiesen wird, eine Ausnahme-Gruppe ist, die alle ausgelösten OSErrors enthält, dann funktioniert der Attributzugriff err.errno nicht mehr. Wir müssten also den Körper der except-Klausel mehrmals ausführen, einmal für jede Ausnahme in der Gruppe. Dies ist jedoch ebenfalls eine potenziell breakende Änderung, da wir derzeit except-Klauseln in der Kenntnis schreiben, dass sie nur einmal ausgeführt werden. Wenn dort eine nicht-idempotente Operation stattfindet, wie z. B. die Freigabe einer Ressource, könnte die Wiederholung schädlich sein.
Die Idee, except über die Blatt-Ausnahmen einer Ausnahme-Gruppe iterieren zu lassen, steht im Mittelpunkt eines alternativen Vorschlags zu diesem PEP von Nathaniel J. Smith, und die Diskussion über diesen Vorschlag erläutert weiter die Fallstricke der Änderung der except-Semantik in einer etablierten Sprache wie Python, sowie die Abweichung von den Semantiken, die parallele Konstrukte in anderen Sprachen haben.
Eine weitere Option, die in der öffentlichen Diskussion aufkam, war die Einführung von except*, aber auch die Behandlung von ExceptionGroups als Sonderfall durch except. except würde dann so etwas wie das Extrahieren einer Ausnahme des passenden Typs aus der Gruppe tun, um sie zu behandeln (während alle anderen Ausnahmen in der Gruppe verworfen werden). Die Motivation hinter diesen Vorschlägen war, die Einführung von Ausnahme-Gruppen sicherer zu machen, da except T Ts abfängt, die in Ausnahme-Gruppen verpackt sind. Wir haben entschieden, dass ein solcher Ansatz die Komplexität der Sprachsemantik erheblich erhöht, ohne sie leistungsfähiger zu machen. Selbst wenn er die Einführung von Ausnahme-Gruppen etwas erleichtern würde (was keineswegs offensichtlich ist), sind dies nicht die Semantiken, die wir uns langfristig wünschen.
Eine neue except-Alternative
Wir erwogen die Einführung eines neuen Schlüsselworts (wie z. B. catch), das verwendet werden kann, um sowohl naked-Ausnahmen als auch Ausnahme-Gruppen zu behandeln. Seine Semantik wäre dieselbe wie die von except* beim Abfangen einer Ausnahme-Gruppe, aber es würde eine naked-Ausnahme nicht zu einer Ausnahme-Gruppe umschließen. Dies wäre Teil eines langfristigen Plans, except durch catch zu ersetzen, aber wir entschieden, dass die Deprekation von except zugunsten eines erweiterten Schlüsselworts für Benutzer zu verwirrend wäre, sodass es angemessener ist, die except*-Syntax für Ausnahme-Gruppen einzuführen, während except weiterhin für einfache Ausnahmen verwendet wird.
Eine except*-Klausel auf eine Exception gleichzeitig anwenden
Wir haben oben erklärt, dass es unsicher ist, eine except-Klausel im bestehenden Code mehr als einmal auszuführen, da der Code möglicherweise nicht idempotent ist. Wir haben erwogen, dies in den neuen except*-Klauseln zu tun, wo die Abwärtskompatibilitätsüberlegungen nicht bestehen. Die Idee ist, eine except*-Klausel immer für eine einzelne Ausnahme auszuführen, wobei möglicherweise dieselbe Klausel mehrmals ausgeführt wird, wenn sie auf mehrere Ausnahmen zutrifft. Wir haben uns stattdessen entschieden, jede except*-Klausel höchstens einmal auszuführen und ihr eine Ausnahme-Gruppe zu geben, die alle passenden Ausnahmen enthält. Der Grund für diese Entscheidung war die Beobachtung, dass, wenn ein Programm den spezifischen Kontext einer Ausnahme kennen muss, die es behandelt, die Ausnahme behandelt wird, bevor sie gruppiert und zusammen mit anderen Ausnahmen ausgelöst wird.
Zum Beispiel ist KeyError eine Ausnahme, die typischerweise mit einer bestimmten Operation zusammenhängt. Jeder Wiederherstellungscode wäre lokal zu dem Ort, an dem der Fehler aufgetreten ist, und würde die traditionelle except-Klausel verwenden.
try:
dct[key]
except KeyError:
# handle the exception
Es ist unwahrscheinlich, dass asyncio-Benutzer so etwas tun möchten.
try:
async with asyncio.TaskGroup() as g:
g.create_task(task1); g.create_task(task2)
except* KeyError:
# handling KeyError here is meaningless, there's
# no context to do anything with it but to log it.
Wenn ein Programm eine Sammlung von Ausnahmen behandelt, die zu einer Ausnahme-Gruppe aggregiert wurden, wird es normalerweise nicht versuchen, von einer bestimmten fehlgeschlagenen Operation wiederherzustellen, sondern wird die Typen der Fehler verwenden, um zu bestimmen, wie sie den Kontrollfluss des Programms beeinflussen sollten oder welche Protokollierung oder Bereinigung erforderlich ist. Diese Entscheidung wird wahrscheinlich dieselbe sein, unabhängig davon, ob die Gruppe eine oder mehrere Instanzen von etwas wie einem KeyboardInterrupt oder asyncio.CancelledError enthält. Daher ist es bequemer, alle Ausnahmen, die mit einer except* übereinstimmen, auf einmal zu behandeln. Wenn dies notwendig wird, kann der Handler die Ausnahme-Gruppe untersuchen und die einzelnen Ausnahmen darin verarbeiten.
Keine Übereinstimmung mit nackten Exceptions in except*
Wir haben die Option in Betracht gezogen, except* T so zu gestalten, dass es nur Ausnahme-Gruppen abgleicht, die Ts enthalten, aber nicht naked Ts. Um zu sehen, warum wir dachten, dass dies keine wünschenswerte Funktion wäre, kehren wir zur Unterscheidung im vorherigen Absatz zwischen Betriebsfehlern und Kontrollfluss-Ausnahmen zurück. Wenn wir nicht wissen, ob wir naked-Ausnahmen oder Ausnahme-Gruppen aus dem Körper eines try-Blocks erwarten sollen, dann sind wir nicht in der Lage, Betriebsfehler zu behandeln. Vielmehr rufen wir wahrscheinlich eine ziemlich generische Funktion auf und werden Fehler behandeln, um Entscheidungen über den Kontrollfluss zu treffen. Wir werden wahrscheinlich dasselbe tun, egal ob wir eine naked-Ausnahme vom Typ T oder eine Ausnahme-Gruppe mit einem oder mehreren Ts abfangen. Daher ist die Last, beides explizit behandeln zu müssen, wahrscheinlich nicht von semantischem Nutzen.
Wenn es sich als notwendig erweist, die Unterscheidung zu treffen, ist es immer möglich, in die try-except*-Klausel eine zusätzliche try-except-Klausel zu verschachteln, die eine naked-Ausnahme abfängt und behandelt, bevor die except*-Klausel sie in eine Ausnahme-Gruppe einpacken kann. In diesem Fall ist der Overhead der Spezifizierung beider keine zusätzliche Belastung – wir müssen wirklich einen separaten Codeblock schreiben, um jeden Fall zu behandeln.
try:
try:
...
except SomeError:
# handle the naked exception
except* SomeError:
# handle the exception group
Mischen von except: und except*: im selben try erlauben
Diese Option wurde abgelehnt, da sie Komplexität hinzufügt, ohne nützliche Semantik. Vermutlich wäre die Absicht, dass ein except T:-Block nur naked-Ausnahmen vom Typ T behandelt, während except* T: T in Ausnahme-Gruppen behandelt. Wir haben bereits oben diskutiert, warum dies in der Praxis wahrscheinlich nicht nützlich ist, und wenn es benötigt wird, kann der verschachtelte try-except-Block stattdessen verwendet werden, um dasselbe Ergebnis zu erzielen.
try* statt except*
Da entweder alle oder keine der Klauseln eines try-Konstrukts except* sind, haben wir erwogen, die Syntax von try anstelle aller except*-Klauseln zu ändern. Wir haben dies abgelehnt, da es weniger offensichtlich wäre. Die Tatsache, dass wir Ausnahme-Gruppen von T anstelle von nur naked Ts behandeln, sollte an derselben Stelle angegeben werden, an der wir T angeben.
Alternative Syntaxoptionen
Alternativen zur except*-Syntax wurden in einer Diskussion auf python-dev evaluiert, und es wurde vorgeschlagen, except group zu verwenden. Nach sorgfältiger Auswertung wurde dies abgelehnt, da das Folgende mehrdeutig wäre, da es derzeit gültige Syntax ist, bei der group als aufrufbar interpretiert wird. Dasselbe gilt für jeden gültigen Bezeichner.
try:
...
except group (T1, T2):
...
Programmierung ohne „except *“
Betrachten Sie das folgende einfache Beispiel der except*-Syntax (vortäuschend, dass Trio diesen Vorschlag nativ unterstützt).
try:
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
except* ValueError:
pass
So würde dieser Code in Python 3.9 aussehen.
def handle_ValueError(exc):
if isinstance(exc, ValueError):
return None
else:
return exc # reraise exc
with MultiError.catch(handle_ValueError):
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
Dieses Beispiel zeigt deutlich, wie unintuitiv und umständlich die Behandlung mehrerer Fehler im aktuellen Python ist. Die Ausnahmebehandlungslogik muss sich in einem separaten Closure befinden und ist ziemlich Low-Level, was vom Autor ein nicht-triviales Verständnis sowohl der Python-Ausnahmenmechanismen als auch der Trio-APIs erfordert. Anstelle des try..except-Blocks müssen wir einen with-Block verwenden. Wir müssen Ausnahmen, die wir nicht behandeln, explizit erneut auslösen. Die Behandlung weiterer Ausnahmetypen oder die Implementierung komplexerer Ausnahmebehandlungslogik wird den Code nur weiter verkomplizieren, bis er unleserlich wird.
Siehe auch
Danksagungen
Wir möchten Nathaniel J. Smith und den anderen Trio-Entwicklern für ihre Arbeit an strukturierter Nebenläufigkeit danken. Wir haben die Idee, einen Ausnahmetree zu konstruieren, dessen Knoten Ausnahmen sind, von MultiError übernommen und die split() API aus dem Design-Dokument für MultiError V2. Die Diskussionen auf python-dev und anderswo haben uns geholfen, den ersten Entwurf des PEP auf vielfältige Weise zu verbessern, sowohl das Design als auch die Darstellung. Dafür wissen wir all jene zu schätzen, die Ideen beigesteuert und gute Fragen gestellt haben: Ammar Askar, Matthew Barnett, Ran Benita, Emily Bowman, Brandt Bucher, Joao Bueno, Baptiste Carvello, Rob Cliffe, Alyssa Coghlan, Steven D’Aprano, Caleb Donovick, Steve Dower, Greg Ewing, Ethan Furman, Pablo Salgado, Jonathan Goble, Joe Gottman, Thomas Grainger, Larry Hastings, Zac Hatfield-Dodds, Chris Jerdonek, Jim Jewett, Sven Kunze, Łukasz Langa, Glenn Linderman, Paul Moore, Antoine Pitrou, Ivan Pozdeev, Patrick Reader, Terry Reedy, Sascha Schlemmer, Barry Scott, Mark Shannon, Damian Shaw, Cameron Simpson, Gregory Smith, Paul Sokolovsky, Calvin Spealman, Steve Stagg, Victor Stinner, Marco Sulla, Petr Viktorin und Barry Warsaw.
Akzeptanz
PEP 654 wurde am 24. September 2021 von Thomas Wouters angenommen.
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-0654.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT