8. Fehler und Ausnahmen
***********************

Bis jetzt wurden Fehlermeldungen nur erwähnt, aber wenn du die
Beispiele ausprobiert hast, hast du wahrscheinlich einige gesehen. Es
gibt (mindestens) zwei Arten von Fehlern, die man unterscheiden kann:
*Syntaxfehler* und *Ausnahmen*.


8.1. Syntax Fehler
==================

Syntaxfehler, auch bekannt als Parsing-Fehler, sind vielleicht die
häufigste Art von Beschwerden, die man bekommt, wenn man Python noch
lernt:

   >>> while True print('Hello world')
     Datei "<stdin>", Zeile 1
       while True print('Hello world')
                  ^^^^^
   SyntaxError: ungültige Syntax

Der Parser wiederholt die fehlerhafte Zeile und zeigt kleine Pfeile
an, die auf die Stelle verweisen, an der der Fehler erkannt wurde.
Beachte, dass dies nicht immer die Stelle ist, die korrigiert werden
muss. Im Beispiel wird der Fehler bei der Funktions "print()" erkannt,
da unmittelbar davor ein Doppelpunkt ("':'") fehlt.

Der Dateiname (in unserem Beispiel "<stdin>" ) und die Zeilennummer
werden angezeigt, damit du weisst, wo du nachsehen musst, falls die
Eingabe aus einer Datei stammt.


8.2. Ausnahmen
==============

Auch wenn eine Anweisung oder ein Ausdruck syntaktisch korrekt ist,
kann es zu einem Fehler kommen, wenn man versucht, sie auszuführen.
Fehler, die während der Ausführung entdeckt werden, werden *Ausnahmen*
genannt und sind nicht unbedingt fatal: Du wirst bald lernen, wie man
sie in Python-Programmen behandelt. Die meisten Ausnahmen werden
jedoch nicht von Programmen behandelt und führen zu Fehlermeldungen
wie hier gezeigt:

   >>> 10 * (1/0)
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 1, in <module>
       10 * (1/0)
             ~^~
   ZeroDivisionError: Division durch Null
   >>> 4 + spam*3
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 1, in <module>
       4 + spam*3
           ^^^^
   NameError: Der Name 'spam' ist nicht definiert
   >>> '2' + 2
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 1, in <module>
       '2' + 2
       ~~~~^~~
   TypeError: Es können nur Zeichenketten (nicht "int") mit Zeichenketten verkettet werden

In der letzten Zeile der Fehlermeldung steht, was passiert ist.
Ausnahmen gibt es in verschiedenen Typen, und der Typ wird als Teil
der Meldung ausgegeben: Die Typen im Beispiel sind
"ZeroDivisionError", "NameError" und "TypeError". Die als Ausnahmetyp
ausgegebene Zeichenkette ist der Name der aufgetretenen eingebauten
Ausnahme. Dies gilt für alle eingebauten Ausnahmen, muss aber nicht
für benutzerdefinierte Ausnahmen gelten (obwohl es eine nützliche
Konvention ist). Standardausnahmenamen sind eingebaute Bezeichner
(keine reservierten Schlüsselwörter).

Der Rest der Zeile enthält detaillierte Informationen über die Art der
Ausnahme und deren Ursache.

Der vorangehende Teil der Fehlermeldung zeigt den Kontext, in dem die
Ausnahme aufgetreten ist, in Form eines Stack-Tracebacks. Im
Allgemeinen enthält er einen Stack-Traceback, der die Zeilen des
Quellcodes auflistet; er zeigt jedoch keine Zeilen an, die von der
Standardeingabe gelesen wurden.

Eingebaute Exceptions listet die eingebauten Ausnahmen und ihre
Bedeutungen auf.


8.3. Behandlung von Ausnahmen
=============================

Es ist möglich, Programme zu schreiben, die ausgewählte Ausnahmen
behandeln. Schau dir das folgende Beispiel an, das den Benutzer zur
Eingabe auffordert, bis ein gültiger Integer eingegeben wurde, dem
Benutzer aber erlaubt, das Programm zu unterbrechen (mit "Control"-"C"
oder was auch immer das Betriebssystem unterstützt); beachte, dass
eine vom Benutzer erzeugte Unterbrechung durch das Auslösen der
Ausnahme "KeyboardInterrupt" signalisiert wird.

   >>> while True:
   ...     try:
   ...         x = int(input("Bitte gib eine Zahl ein: "))
   ...         break
   ...     except ValueError:
   ...         print("Hoppla! Das war keine gültige Zahl. Versuche es noch einmal...")
   ...

Die "try" Anweisung funktioniert wie folgt.

* Zuerst wird die *Try-Klausel* (Die Anweisung(en) zwischen den
  Schlüsselwörtern "try" und "except") ausgeführt.

* Wenn keine Ausnahme auftritt, wird die *except clause* übersprungen
  und die Ausführung von der "try" Anweisung beendet.

* Wenn während der Ausführung der "try" Klausel eine Ausnahme
  auftritt, wird der Rest der Klausel übersprungen. Wenn der Typ der
  Ausnahme mit der nach dem "except" Schlüsselwort genannten Ausnahme
  übereinstimmt, wird die *Except-Klausel* ausgeführt, und die
  Ausführung wird nach dem try/except-Block fortgesetzt.

* Tritt eine Ausnahme auf, die nicht mit der in der *Except-Klausel*
  genannten Ausnahme übereinstimmt, wird sie an äußere "try"
  Anweisungen weitergegeben; wird kein Handler gefunden, handelt es
  sich um eine *unbehandelte Ausnahme* und die Ausführung wird mit
  einer Fehlermeldung beendet.

A "try" statement may have more than one *except clause*, to specify
handlers for different exceptions.  At most one handler will be
executed. Handlers only handle exceptions that occur in the
corresponding *try clause*, not in other handlers of the same "try"
statement.  An *except clause* may name multiple exceptions as a
parenthesized tuple, for example:

   ... except (RuntimeError, TypeError, NameError):
   ...     pass

Eine Klasse in einer  "except" -Klausel deckt Ausnahmen ab, bei denen
es sich um Instanzen dieser Klasse selbst oder einer ihrer
abgeleiteten Klassen handelt (jedoch nicht umgekehrt – eine
*except“-Klausel*, in der eine abgeleitete Klasse aufgeführt ist,
deckt keine Instanzen ihrer Basisklassen ab). Der folgende Code gibt
beispielsweise B, C und D in dieser Reihenfolge aus:

   class B(Exception):
       pass

   class C(B):
       pass

   class D(C):
       pass

   for cls in [B, C, D]:
       try:
           raise cls()
       except D:
           print("D")
       except C:
           print("C")
       except B:
           print("B")

Beachte, dass, wenn die *Exception-Klauseln* umgekehrt wären (mit
"except B" zuerst), B, B, B ausgegeben worden wäre --- die erste
passende *Exception-Klausel* wird ausgelöst.

Wenn eine Ausnahme auftritt, kann sie mit Werten verbunden sein, die
auch als *Argumente* der Ausnahme bezeichnet werden. Das Vorhandensein
und die Art der Argumente hängen von der Art der Ausnahme ab.

In der *Except-Klausel* kann nach dem Namen der Ausnahme eine Variable
angegeben werden. Die Variable ist an die Ausnahmen-Instanz gebunden,
die typischerweise ein "args" Attribut hat, das die Argumente
speichert. Der Einfachheit halber definieren eingebaute Ausnahmetypen
"__str__()", um alle Argumente auszugeben, ohne explizit auf ".args"
zuzugreifen.

   >>> try:
   ...     raise Exception('spam', 'eggs')
   ... except Exception as inst:
   ...     print(type(inst))    # der Exception-Typ
   ...     print(inst.args)     # in .args gespeicherte Argumentes
   ...     print(inst)          # __str__ erlaubt die direkte Ausgabe von args,
   ...                          # kann aber in Exception-Unterklassen überschrieben werden
   ...     x, y = inst.args     # args entpacken
   ...     print('x =', x)
   ...     print('y =', y)
   ...
   <class 'Exception'>
   ('spam', 'eggs')
   ('spam', 'eggs')
   x = spam
   y = eggs

Bei unbehandelten Ausnahmen wird die "__str__()" Ausgabe der Ausnahme
als letzter Teil ("Detail") der Meldung ausgegeben.

"BaseException" ist die gemeinsame Basisklasse für alle Ausnahmen.
Eine ihrer Unterklassen, "Exception", ist die Basisklasse für alle
nicht schwerwiegenden Ausnahmen. Ausnahmen, die keine Unterklassen von
"Exception" sind, werden in der Regel nicht behandelt, da sie dazu
dienen, anzuzeigen, dass das Programm beendet werden soll. Dazu gehört
die "SystemExit" Ausnahme, die von "sys.exit()" und
"KeyboardInterrupt" ausgelöst wird, wenn ein Benutzer das Programm
unterbrechen möchte.

"Exception" kann als Platzhalter verwendet werden, der (fast) alles
abfängt. Es empfiehlt sich jedoch, die Arten von Ausnahmen, die
behandelt werden sollen, so genau wie möglich festzulegen und
zuzulassen, dass unerwartete Ausnahmen weitergegeben werden.

Das gängigste Muster für die "Exception" Behandlung ist, die Ausnahme
auszugeben oder zu protokollieren und sie dann erneut auszulösen
(damit ein Aufrufer die Ausnahme ebenfalls behandeln kann):

   import sys

   try:
       f = open('myfile.txt')
       s = f.readline()
       i = int(s.strip())
   except OSError as err:
       print("OS-Fehler:", err)
   except ValueError:
       print("Daten konnten nicht in eine Ganzzahl konvertiert werden.")
   except Exception as err:
       print(f"Unerwarteter {err=}, {type(err)=}")
       raise

Die "try" ... "except" Anweisung hat eine optionale *Else-Klausel*,
die, wenn vorhanden, allen *Except-Klauseln* folgen muss. Sie ist
nützlich für Code, der ausgeführt werden muss, wenn die *Try-Klausel*
keine Ausnahme auslöst. Zum Beispiel:

   for arg in sys.argv[1:]:
       try:
           f = open(arg, 'r')
       except OSError:
           print('kann nicht geöffnet werden', arg)
       else:
           print(arg, 'hat', len(f.readlines()), 'Zeilen')
           f.close()

Die Verwendung der "else" Klausel ist besser als das Hinzufügen von
zusätzlichem Code zu der "try" Klausel, weil dadurch vermieden wird,
dass versehentlich eine Ausnahme abgefangen wird, die nicht durch den
Code ausgelöst wurde, der durch die "try" ... "except" Anweisung
geschützt wird.

Ausnahmehandler behandeln nicht nur Ausnahmen, die unmittelbar in der
*Try-Klausel* auftreten, sondern auch solche, die innerhalb von
Funktionen auftreten, die (auch indirekt) in der *Try-Klausel*
aufgerufen werden. Zum Beispiel:

   >>> def this_fails():
   ...     x = 1/0
   ...
   >>> try:
   ...     this_fails()
   ... except ZeroDivisionError as err:
   ...     print('Behandlung eines Laufzeitfehlers:', err)
   ...
   Behandlung eines Laufzeitfehlers: Division durch Null


8.4. Auslösen von Ausnahmen
===========================

Die "raise" Anweisung ermöglicht es dem Programmierer, das Auftreten
einer bestimmten Ausnahme zu erzwingen. Zum Beispiel:

   >>> raise NameError('HiThere')
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 1, in <module>
       raise NameError('HiThere')
   NameError: HiThere

Das reine "raise" Argument gibt die Ausnahme an, die ausgelöst werden
soll. Dabei muss es sich entweder um eine Ausnahme-Instanz oder eine
Ausnahmen-Klasse handeln (Eine Klasse, die von einer "BaseException",
wie "Exception" oder einer ihrer Unterklassen abgeleitet ist). Wenn
eine Ausnahmeklasse übergeben wird, wird sie implizit instanziiert,
indem ihr Konstruktor ohne Argumente aufgerufen wird:

   raise ValueError  # Kurzform für 'raise ValueError()'

Wenn du feststellen musst, ob eine Ausnahme ausgelöst wurde, aber
nicht beabsichtigst, diese zu behandeln, kannst du mit einer
einfacheren Form der "raise" Anweisung die Ausnahme erneut auslösen:

   >>> try:
   ...     raise NameError('HiThere')
   ... except NameError:
   ...     print('Eine Ausnahme ist aufgetreten!')
   ...     raise
   ...
   Eine Ausnahme ist aufgetreten!
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 2, in <module>
       raise NameError('HiThere')
   NameError: HiThere


8.5. Verkettung von Ausnahmen
=============================

Tritt eine unbehandelte Ausnahme innerhalb eines "except" Abschnitts
auf, wird die behandelte Ausnahme an diesen Abschnitt angehängt und in
die Fehlermeldung aufgenommen:

   >>> try:
   ...     open("database.sqlite")
   ... except OSError:
   ...     raise RuntimeError("unable to handle error")
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       open("database.sqlite")
       ~~~~^^^^^^^^^^^^^^^^^^^
   FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

   During handling of the above exception, another exception occurred:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError("unable to handle error")
   RuntimeError: unable to handle error

Um anzuzeigen, dass eine Ausnahme eine direkte Folge einer anderen
ist, erlaubt die "raise" Anweisung eine optionale "from" Klausel:

   # exc muss eine Ausnahminstanz oder None sein.
   raise RuntimeError from exc

Dies kann nützlich sein, wenn du Ausnahmen umwandeln wikkst. Zum
Beispiel:

   >>> def func():
   ...     raise ConnectionError
   ...
   >>> try:
   ...     func()
   ... except ConnectionError as exc:
   ...     raise RuntimeError('Failed to open database') from exc
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       func()
       ~~~~^^
     File "<stdin>", line 2, in func
   ConnectionError

   The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError('Failed to open database') from exc
   RuntimeError: Failed to open database

Es erlaubt auch die Deaktivierung der automatischen Ausnahmeverkettung
mit dem "from None" Idiom:

   >>> try:
   ...     open('database.sqlite')
   ... except OSError:
   ...     raise RuntimeError from None
   ...
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 4, in <module>
       raise RuntimeError from None
   RuntimeError

Weitere Informationen zur Verkettungsmechanik findest du unter
Eingebaute Exceptions.


8.6. Benutzerdefinierte Ausnahmen
=================================

Programme können ihre eigenen Ausnahmen benennen, indem sie eine neue
Ausnahmeklasse erstellen (siehe Klassen für mehr über Python-Klassen).
Ausnahmen sollten in der Regel von der "Exception" Klasse abgeleitet
werden, entweder direkt oder indirekt.

Es können Ausnahmeklassen definiert werden, die alles tun können, was
jede andere Klasse auch tun kann, aber sie sind in der Regel einfach
gehalten und bieten oft nur eine Reihe von Attributen, die es
ermöglichen, Informationen über den Fehler durch Handler für die
Ausnahme zu extrahieren.

Die meisten Ausnahmen werden mit Namen definiert, die auf "Error"
enden, ähnlich der Benennung der Standardausnahmen.

Viele Standardmodule definieren ihre eigenen Ausnahmen, um Fehler zu
melden, die in den von ihnen definierten Funktionen auftreten können.


8.7. Definition von Bereinigungsaktionen
========================================

Die "try" Anweisung enthält eine weitere optionale Klausel, die dazu
dient, Bereinigungsaktionen zu definieren, die unter allen Umständen
ausgeführt werden müssen. Zum Beispiel:

   >>> try:
   ...     raise KeyboardInterrupt
   ... finally:
   ...     print('Auf Wiedersehen, Welt!')
   ...
   Auf Wiedersehen, Welt!
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 2, in <module>
       raise KeyboardInterrupt
   KeyboardInterrupt

Wenn eine "finally" Klausel vorhanden ist, wird die "finally" Klausel
als letzte Aufgabe ausgeführt, bevor die "try" Anweisung abgeschlossen
wird. Die "finally" Klausel wird unabhängig davon ausgeführt, ob die
"try" Anweisung eine Ausnahme erzeugt oder nicht. In den folgenden
Punkten werden komplexere Fälle beim Auftreten einer Ausnahme
behandelt:

* Wenn während der Ausführung der "try" Klausel eine Ausnahme
  auftritt, kann die Ausnahme von einer "except" Klausel behandelt
  werden. Wenn die Ausnahme nicht durch eine "except" Klausel
  behandelt wird, wird die Ausnahme erneut ausgelöst, nachdem die
  "finally" Klausel ausgeführt wurde.

* Während der Ausführung einer "except" oder "else" Klausel kann eine
  Ausnahme auftreten. Auch hier wird die Ausnahme erneut ausgelöst,
  nachdem die "finally" Klausel ausgeführt wurde.

* If the "finally" clause executes a "break", "continue" or "return"
  statement, exceptions are not re-raised.

* Wenn die "try" Anweisung eine "break", "continue" oder "return"
  Anweisung erreicht, wird die "finally" Klausel direkt vor der
  Ausführung der "break", "continue" oder "return" Anweisung
  ausgeführt.

* If a "finally" clause includes a "return" statement, the returned
  value will be the one from the "finally" clause's "return"
  statement, not the value from the "try" clause's "return" statement.

Zum Beispiel:

   >>> def bool_return():
   ...     try:
   ...         return True
   ...     finally:
   ...         return False
   ...
   >>> bool_return()
   False

Ein komplizierteres Beispiel:

   >>> def divide(x, y):
   ...     try:
   ...         result = x / y
   ...     except ZeroDivisionError:
   ...         print("Division durch Null!")
   ...     else:
   ...         print("Ergebnis ist", result)
   ...     finally:
   ...         print("Ausführung der finally-Klausel")
   ...
   >>> divide(2, 1)
   Ergebnis ist 2.0
   Ausführung der finally-Klausel
   >>> divide(2, 0)
   Division durch Null!
   Ausführung der finally-Klausel
   >>> divide("2", "1")
   Ausführung der finally-Klausel
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 1, in <module>
       divide("2", "1")
       ~~~~~~^^^^^^^^^^
     Datei "<stdin>", Zeile 3, in divide
       result = x / y
                ~~^~~
   TypeError: Nicht unterstützte Operandentypen für /: 'str' und 'str'

Wie du sehen kannst, wird die "finally" Klausel in jedem Fall
ausgeführt. Der "TypeError", ausgelöst durch die Teilung zweier
Zeichenfolgenn wird von der "except" Klausel nicht verarbeitet und
daher nach Ausführung der "finally" Klausel erneut ausgelöst.

In realen Anwendungen ist die "finally" Klausel nützlich, um externe
Ressourcen (z. B. Dateien oder Netzwerkverbindungen) freizugeben,
unabhängig davon, ob die Nutzung der Ressource erfolgreich war.


8.8. Vordefinierte Bereinigungsaktionen
=======================================

Einige Objekte definieren Standardbereinigungsaktionen, die
durchgeführt werden, wenn das Objekt nicht mehr benötigt wird,
unabhängig davon, ob der Vorgang mit dem Objekt erfolgreich war oder
fehlgeschlagen ist. Schauen Sie sich das folgende Beispiel an, in dem
versucht wird, eine Datei zu öffnen und ihren Inhalt auf dem
Bildschirm auszugeben.

   for line in open("myfile.txt"):
       print(line, end="")

Das Problem bei diesem Code besteht darin, dass die Datei für eine
unbestimmte Zeit geöffnet bleibt, nachdem die Ausführung dieses Teils
des Codes abgeschlossen ist. Dies ist bei einfachen Skripten kein
Problem, kann jedoch bei größeren Anwendungen ein Problem darstellen.
Die "with" Anweisung ermöglicht die Verwendung von Objekten wie
Dateien auf eine Weise, die sicherstellt, dass sie stets zeitnah und
korrekt bereinigt werden.

   with open("myfile.txt") as f:
       for line in f:
           print(line, end="")

Nach Ausführung der Anweisung wird die Datei *f* immer geschlossen,
auch wenn bei der Verarbeitung der Zeilen ein Problem aufgetreten ist.
Objekte, die wie Dateien vordefinierte Bereinigungsaktionen
bereitstellen, weisen in ihrer Dokumentation darauf hin.


8.9. Auslösen und Behandeln mehrerer unzusammenhängender Ausnahmen
==================================================================

Es gibt Situationen, in denen es notwendig ist, mehrere aufgetretene
Ausnahmen zu melden. Dies ist häufig in Parallelitäts-Frameworks der
Fall, wenn möglicherweise mehrere Aufgaben parallel fehlgeschlagen
sind. Es gibt jedoch auch andere Anwendungsfälle, in denen es
wünschenswert ist, die Ausführung fortzusetzen und mehrere Fehler zu
sammeln, anstatt die erste Ausnahme auszulösen.

Die integrierte "ExceptionGroup" Funktion umschließt eine Liste von
Ausnahmeinstanzen, sodass diese gemeinsam ausgelöst werden können. Da
es sich selbst um eine Ausnahme handelt, kann sie wie jede andere
Ausnahme abgefangen werden.

   >>> def f():
   ...     excs = [OSError('error 1'), SystemError('error 2')]
   ...     raise ExceptionGroup('there were problems', excs)
   ...
   >>> f()
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 1, in <module>
     |     f()
     |     ~^^
     |   File "<stdin>", line 3, in f
     |     raise ExceptionGroup('there were problems', excs)
     | ExceptionGroup: there were problems (2 sub-exceptions)
     +-+---------------- 1 ----------------
       | OSError: error 1
       +---------------- 2 ----------------
       | SystemError: error 2
       +------------------------------------
   >>> try:
   ...     f()
   ... except Exception as e:
   ...     print(f'caught {type(e)}: {e}')
   ...
   caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)
   >>>

Durch das Nutzen von "except*" anstelle von "except", können wir
selektiv nur die Ausnahmen in der Gruppe behandeln, die einen
bestimmten Typ entsprechen. Im folgenden Beispiel, das eine
verschachtelte Ausnahmegruppe zeigt, extrahiert jede "except*" Klausel
Ausnahmen eines bestimmten Typs aus der Gruppe, während alle anderen
Ausnahmen an andere Klauseln weitergegeben und schließlich erneut
ausgelöst werden.

   >>> def f():
   ...     raise ExceptionGroup(
   ...         "group1",
   ...         [
   ...             OSError(1),
   ...             SystemError(2),
   ...             ExceptionGroup(
   ...                 group2,
   ...                 [
   ...                     OSError(3),
   ...                     RecursionError(4)
   ...                 ]
   ...             )
   ...         ]
   ...     )
   ...
   >>> try:
   ...     f()
   ... except* OSError as e:
   ...     print("Es gab OSErrors")
   ... except* SystemError as e:
   ...     print("Es gab SystemErrors")
   ...
   Es sind OSErrors aufgetreten
   Es sind SystemErrors aufgetreten
     + Traceback der Ausnahmegruppe (letzter Aufruf zuletzt):
     |   Datei "<stdin>", Zeile 2, in <module>
     |     f()
     |     ~^^
     |   Datei "<stdin>", Zeile 2, in f
     |     raise ExceptionGroup(
     |     ...<12 lines>...
     |     )
     | ExceptionGroup: group1 (1 Unterausnahme)
     +-+---------------- 1 ----------------
       | ExceptionGroup: group2 (1 Unterausnahme)
       +-+---------------- 1 ----------------
         | RecursionError: 4
         +------------------------------------
   >>>

Beachte, dass die in einer Ausnahmegruppe verschachtelten Ausnahmen
Instanzen und keine Typen sein müssen. Dies liegt daran, dass es sich
in der Praxis typischerweise um Ausnahmen handelt, die bereits vom
Programm ausgelöst und abgefangen wurden, und zwar nach dem folgenden
Muster:

   >>> excs = []
   ... for test in tests:
   ...     try:
   ...         test.run()
   ...     except Exception as e:
   ...         excs.append(e)
   ...
   >>> if excs:
   ...    raise ExceptionGroup("Testfehler", excs)
   ...


8.10. Anreicherung von Ausnahmen mit Notizen
============================================

Wenn eine Ausnahme erstellt wird, um ausgelöst zu werden, wird sie
normalerweise mit Informationen initialisiert, die den aufgetretenen
Fehler beschreiben. Es gibt Fälle, in denen es sinnvoll ist,
Informationen hinzuzufügen, nachdem die Ausnahme abgefangen wurde. Zu
diesem Zweck verfügen Ausnahmen über eine Methode "add_note(note)",
welche einen String akzeptiert und zur Notizenliste der Ausnahme
hinzufügt. Das Standard-Traceback-Rendering umfasst alle Notizen in
der Reihenfolge, in der sie nach der Ausnahme hinzugefügt wurden.

   >>> try:
   ...     raise TypeError('falscher Typ')
   ... except Exception as e:
   ...     e.add_note('Einige Informationen hinzufügen')
   ...     e.add_note('Weitere Informationen hinzufügen')
   ...     raise
   ...
   Traceback (letzter Aufruf zuletzt):
     Datei "<stdin>", Zeile 2, in <module>
       raise TypeError('falscher Typ')
   TypeError: falscher Typ
   Füge einige Informationen hinzu
   Füge noch mehr Informationen hinzu
   >>>

Wenn wir beispielsweise Ausnahmen in einer Ausnahmegruppe sammeln,
möchten wir möglicherweise Kontextinformationen für die einzelnen
Fehler hinzufügen. Im Folgenden gibt es für jede Ausnahme in der
Gruppe einen Hinweis, der angibt, wann dieser Fehler aufgetreten ist.

   >>> def f():
   ...     raise OSError('Vorgang fehlgeschlagen')
   ...
   >>> excs = []
   >>> for i in range(3):
   ...     try:
   ...         f()
   ...     except Exception as e:
   ...         e.add_note(f'Aufgetreten in Iteration {i+1}')
   ...         excs.append(e)
   ...
   >>> raise ExceptionGroup('Es sind einige Probleme aufgetreten', excs)
     + Traceback der Ausnahmegruppe (letzter Aufruf zuletzt):
     |   Datei "<stdin>", Zeile 1, in <module>
     |     raise ExceptionGroup('Wir haben einige Probleme', excs)
     | ExceptionGroup: Wir haben einige Probleme (3 Unterausnahmen)
     +-+---------------- 1 ----------------
       | Traceback (letzter Aufruf zuletzt):
       |   Datei "<stdin>", Zeile 3, in <module>
       |     f()
       |     ~^^
       |   Datei "<stdin>", Zeile 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Aufgetreten in Iteration 1
       +---------------- 2 ----------------
       | Traceback (letzter Aufruf zuletzt):
       |   Datei "<stdin>", Zeile 3, in <module>
       |     f()
       |     ~^^
       |   Datei "<stdin>", Zeile 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Aufgetreten in Iteration 2
       +---------------- 3 ----------------
       | Traceback (letzter Aufruf zuletzt):
       |   Datei "<stdin>", Zeile 3, in <module>
       |     f()
       |     ~^^
       |   Datei "<stdin>", Zeile 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Aufgetreten in Iteration 3
       +------------------------------------
   >>>
