Exceptions#

Midjourney: Exception in Time, ref. Salvador Dalí

Anything that can go wrong, will go wrong.

— Murphy’s Law #1

Folien/PDF#

Fehler mit Exceptions behandeln#

Exceptions unterbrechen den normalen Verlauf eines Programmes in einem Fehlerfall. Sie dienen dazu den Fehler im Programm zu kommunizieren (sie haben meist eine Fehlermeldung) und werden genutzt, um im Fehlerfall unkontrolliertes Abstürzen des Programmes zu verhindern.

Bekannte Exceptions sollten immer abgefangen. In Python werden sie durch den try-except-Block abgefangen. Dieser startet mit einem try und endet mit einem except. Nach dem try folgt der Block mit der bekannten (oder unbekannten) Exception. Nach dem except folgt der Block zur Fehlerbehandlung.

Es ist zu beachten, dass bei Exceptions keine Wertzuweisung stattfindet (der Wert ist ja nicht bekannt). Betrachten wir mal wieder, die aus der Verzweigung bekannte Division durch 0 mit ihren verschiedenen Fehlerfällen der Null-Division und falscher Datentypen der Eingaben. Ohne diese Fehlerfälle genau zu spezifizieren können wir schreiben.

zaehler = 10
nenner = 0
ergebnis = None
try:
    # Block mit bekannter Exception
    ergebnis = zaehler / nenner
    # Dieser Teil wird nur ausgeführt wenn keine Exception auftrat
    print("Die Division war erfolgreich")
except:
    # Fehlerbehandlung
    print("Divisionsfehler")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler
Das Ergebnis von 10/0 = None

Das except is in dem obigen Fall untypisiert. Damit lassen sich alle Fehler, die auftreten können abfangen (auch unbekannte). Damit fängt der try-except-Block auch Fehler eines falschen Datentyps ab.

zaehler = 'keine_zahl'
nenner = 2
ergebnis = None
try:
    ergebnis = zaehler / nenner
except:
    print("Divisionsfehler")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler
Das Ergebnis von keine_zahl/2 = None

Das Problem hierbei ist, dass wir keine Information über die Exception erhalten. Deshalb ist es immer sinnvoll die genaue Exception abzufangen und die Fehlermeldung auszugeben. Der allgemeinste Fehlertyp in Python ist die Exception welche wir wie folgt als Variable e abfangen.

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except Exception as e:
    print(f"Divisionsfehler mit Fehler vom typ `{type(e)}` und Meldung `{e}`")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler mit Fehler vom typ `<class 'TypeError'>` und Meldung `unsupported operand type(s) for /: 'str' and 'int'`
Das Ergebnis von keine_zahl/0 = None

Sind die Exceptions bekannt sollten sie auch typisiert behandelt werden, um spezifische Meldungen auszugeben.

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Zaehler oder Nenner nicht vom Datentyp `int`
Das Ergebnis von keine_zahl/0 = None

Der try-except-Block unterstütz auch die else-Anweisung, welche immer ausgeführt wird, wenn keine Exception auftritt. Ferner gibt es die finally-Anweisung, welche immer ausgeführt wird also im fehlerfreien Fall und im Fehlerfall.

Soll zum Beispiel eine Ausgabe nur gemacht werden, wenn der try-Block erfolgreich war, nutzen wir else.

zaehler = 2
nenner = 1
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")
else:
    print(f"Die Division war erfolgreich und resultiert in {zaehler}/{nenner} = {ergebnis}")
Die Division war erfolgreich und resultiert in 2/1 = 2.0

Soll immer eine Ausgabe erfolgen können wir finally verwenden. Dies wird häufig verwendet, um Schritte durchzuführen die auch im Fehlerfall gemacht werden sollen, z.B. um Verbindungen zu Dateien oder zu einer Datenbank zu schließen, damit diese nicht unendlich lange offen bleiben (und irgendwann zu Fehlern führen).

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")
finally:
    print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Zaehler oder Nenner nicht vom Datentyp `int`
Das Ergebnis von keine_zahl/0 = None

Exceptions selbst erzeugen#

Exceptions können auch selbst mit raise erzeugt werden. Das ist sinnvoll, wenn in dem eigenen Code Fehler auftreten können, die woanders behandelt werden müssen. Hierbei muss ein Fehlertyp angegeben werden. Entweder man nutzt den allgemeinsten Fehlertyp Exception, einen passenden Standardfehler von Python oder einen selbst definierten. Es ist ratsam meist spezifische Fehlertypen zu nutzen, da der Typ des Fehlers viel beim Behandeln und Debuggen hilft.

Definieren wir als Beispiel unsere eigene Divisionsfunktion mit klarer Fehlerbenenung ob Zähler oder Nenner vom falschen Typ sind. Hier wollen wir, dass anstatt des TypeError ein ValueError erzeugt wird. Bei einer Division durch 0 soll anstatt eines Fehlers der Wert None zurückgegeben werden.

def division(zaehler, nenner):
    if not isinstance(nenner, (int, float)):
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
    elif not isinstance(zaehler, (int, float)):
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
    elif nenner == 0:
        print("Warnung Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis
zaehler = 'keine_zahl'
nenner = 0

ergebnis = division(zaehler, nenner)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 4
      1 zaehler = 'keine_zahl'
      2 nenner = 0
----> 4 ergebnis = division(zaehler, nenner)

Cell In[7], line 5, in division(zaehler, nenner)
      3     raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
      4 elif not isinstance(zaehler, (int, float)):
----> 5     raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
      6 elif nenner == 0:
      7     print("Warnung Division durch 0")

ValueError: Zaehler nicht vom Datentyp `int` oder `float`
zaehler= 10
nenner = 2
zaehler = 10
nenner = 1

ergebnis = division(zaehler, nenner)
Das Ergebnis von 10/1 = 10.0

Fehlerbehandlung bei Funktionen im Stack#

Exceptions werden im Stack (Aufrufreihenfolge der Funktionen) nach oben weitergegeben, bis sie entweder abgefangen und behandelt werden oder das Programm abstürzt, wenn das Ende des Stacks erreicht wird. Ziel des Abfangens und Behandelns von Exceptions ist es das Programm zurück in einen Zustand zu bringen in dem es weiterlaufen kann.

Als Beispiel wollen wir eine rekursive Funktion definieren, welche uns einen Fehler ausgibt, wenn die Rekurssionstiefe zu groß wird um einen Stack-Overflow-Fehler zu vermeiden. Wir nutzen als Beispiel die Fakultät aus dem Rekursionbeispiel.

def factorial_recursiv(x, depth=1):
    if depth > 20:
        raise RecursionError("Recursion zu tief")
    if x > 1:
        return x * factorial_recursiv(x-1, depth+1)
    else:
        return 1
factorial_recursiv(20)
2432902008176640000
factorial_recursiv(21)
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
Cell In[13], line 1
----> 1 factorial_recursiv(21)

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

    [... skipping similar frames: factorial_recursiv at line 5 (17 times)]

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

Cell In[11], line 3, in factorial_recursiv(x, depth)
      1 def factorial_recursiv(x, depth=1):
      2     if depth > 20:
----> 3         raise RecursionError("Recursion zu tief")
      4     if x > 1:
      5         return x * factorial_recursiv(x-1, depth+1)

RecursionError: Recursion zu tief

In der obigen Fehlermeldung bringt das Programm selbst zum erliegen (es stürtzt allerdings nicht der ganze Kernel ab, wie beim Stack-Overflow-Fehler). Um das zu vermeiden müssen wir den Fehler mit try-except abfangen. In der Fehlermeldung oben deutet sich schon der Stack-Trace an, also die Liste der Funktionsaufrufe die auf dem Stack gesammelt wurde.

import traceback
try:
    factorial_recursiv(21)
except RecursionError as e:
    print(e)
    traceback.print_exc()
Recursion zu tief
Traceback (most recent call last):
  File "/var/folders/0c/805v004947lf04d_w7xm50rc0000gn/T/ipykernel_18930/841887965.py", line 3, in <module>
    factorial_recursiv(21)
  File "/var/folders/0c/805v004947lf04d_w7xm50rc0000gn/T/ipykernel_18930/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/folders/0c/805v004947lf04d_w7xm50rc0000gn/T/ipykernel_18930/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/folders/0c/805v004947lf04d_w7xm50rc0000gn/T/ipykernel_18930/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 17 more times]
  File "/var/folders/0c/805v004947lf04d_w7xm50rc0000gn/T/ipykernel_18930/43912226.py", line 3, in factorial_recursiv
    raise RecursionError("Recursion zu tief")
RecursionError: Recursion zu tief

Debugging mit print()#

Logische Fehler treten häufig erst dynamisch auf und lassen sich nicht durch statische Code-Analyse durch Lint-Tools finden. Hier muss man dann den aktuellen Programmfluss nachvollziehen. Dies nennt man Debugging. Die einfachste Form ist das print-Debugging bei der man den Code mit print()-Befehlen zuspammt.

Wir erweitern mal unsere Divisionsfunktion mit schönen vielen print()-Statements. Üblich ist es zum Beispiel, die Eingangsparameter auszugeben, Fehler und Warnungen auszugeben, als auch dann Ergebnisse zu loggen.

def division(zaehler, nenner):
    print(f"Debug: Eingabe Zaehler: {zaehler}")
    print(f"Debug: Eingabe Nenner: {nenner}")
    if not isinstance(nenner, (int, float)):
        print(f"Error: Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
    elif not isinstance(zaehler, (int, float)):
        print(f"Error: Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
    elif not nenner:
        print("Warning: Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        print(f"Info: Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis

Jetzt können wir insbesondere im Fehlerfall sehr gut nachvollziehen was genau geschehen ist. Zum Beispiel bei der Division durch 0.

division(10, 0)
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 0
Warning: Division durch 0

Allerdings haben wir auch im korrekten Fall sehr viele Ausgaben. Das kann sehr störend sein, weil man dann richtige Fehler sehr schnell übersieht. Zum Beispiel erzeugen wir zehn Divisionen, von der eine eine Division durch 0 war.

for nenner in range(-2, 8):
    division(10, nenner)
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: -2
Info: Das Ergebnis von 10/-2 = -5.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: -1
Info: Das Ergebnis von 10/-1 = -10.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 0
Warning: Division durch 0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 1
Info: Das Ergebnis von 10/1 = 10.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 2
Info: Das Ergebnis von 10/2 = 5.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 3
Info: Das Ergebnis von 10/3 = 3.3333333333333335
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 4
Info: Das Ergebnis von 10/4 = 2.5
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 5
Info: Das Ergebnis von 10/5 = 2.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 6
Info: Das Ergebnis von 10/6 = 1.6666666666666667
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 7
Info: Das Ergebnis von 10/7 = 1.4285714285714286

Debugging mit logging#

Deshalb verwendet man bei komplexeren Programmen meist ein logging-Paket. Diese erlauben es print-Statements Kategorien zuzuordnen und anhand dieser zu filtern. In der Python-Bibliothek logging sind die Kategorien: debug, info, warning, error und critical.

import logging

log=logging.getLogger("meinlog")

def division(zaehler, nenner):
    log.debug(f"Eingabe Zaehler: {zaehler}")
    log.debug(f"Eingabe Nenner: {nenner}")
    if not isinstance(nenner, (int, float)):
        log.error(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
    elif not isinstance(zaehler, (int, float)):
        log.error(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
    elif not nenner:
        log.warning("Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        log.info(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis

Wenn wir jetzt die Funktion aufrufen, sehen wir nur noch die Division durch 0 Warnung.

log.setLevel(logging.WARNING)
for nenner in range(-2, 8):
    division(10, nenner)
Division durch 0

Wir können allerdings bei Bedarf, wie bei der Fehlersuche den Loglevel auch erhöhen. Zum Beispiel wollen wir alle Debug-Nachrichten erhalten.

log.setLevel(logging.DEBUG)
for nenner in range(-2, 8):
    division(10, nenner)
Division durch 0

Ferner erlaub das Logging auch automatisch weitere Informationen hinzuzufügen. Wir sehen im Log oben bereits, das nicht nur das Level (INFO, DEBUG, WARNING) sondern auch den Namen des Loggers (meinlog). Wir können dieses Format anpassen um z.B. auch die Zeit auszugeben, was insbesondere wichtig ist um zu verstehen wann etwas passiert ist.

sh=logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s  %(name)s  %(levelname)s: %(message)s')
sh.setFormatter(formatter)
log.addHandler(sh)
log.setLevel(logging.INFO)

for nenner in range(-2, 8):
    division(10, nenner)
2025-08-17 14:13:15,967  meinlog  INFO: Das Ergebnis von 10/-2 = -5.0
2025-08-17 14:13:15,967  meinlog  INFO: Das Ergebnis von 10/-1 = -10.0
2025-08-17 14:13:15,967  meinlog  WARNING: Division durch 0
2025-08-17 14:13:15,967  meinlog  INFO: Das Ergebnis von 10/1 = 10.0
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/2 = 5.0
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/3 = 3.3333333333333335
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/4 = 2.5
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/5 = 2.0
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/6 = 1.6666666666666667
2025-08-17 14:13:15,968  meinlog  INFO: Das Ergebnis von 10/7 = 1.4285714285714286

In der Praxis wird das Logging sehr häufig insbesondere in Cloud-Anwendungen verwendet. Da diese ja keine Bildschirme haben, müssen Fehler in Logs gesucht werden. Solange alles ok ist, läuft so eine Anwendung dann z.B. im Log-Level INFO, mit nur wenig Ausgaben. Tritt ein Fehler auf, so wird der Server auf das Log-Level DEBUG gesetzt und man sucht in den detaillierten Logs dann nach Informationen, um den Fehler einzugrenzen.

Debugging durch Debug-Oberflächen#

Viele integrierte Entwicklungsumgebungen (IDE) bieten an direkt Debugger auszuführen. Diese Debugger erlauben es die dynamische Ausführung des Codes zu unterbrechen. Ziel ist es kurz vor Auftreten des Fehlers die Ausführung anzuhalten, um dann das Fehlerverhalten genau beobachten zu können.

Dabei werden meist zwei Formen der Unterbrechung unterstützt:

  • Die Unterbrechung in bestimmten Code-Zeilen mit Hilfe von Breakpoints.

  • Die Unterbrechung bei bestimmten Exceptions.

Debugging in Jupyter-Notebooks in VSCode#

Die Debug-Oberflächen sehen je nach IDE etwas anders aus, verfügen aber über ähnliche Funktionen. Dieses Jupyter Notebook wurde in VSCode geschrieben, welches wir als erstes Beispiel betrachten.

Dafür kann man meist in der IDE links neben eine Zeile klicken um einen Breakpoint zu setzen. Wir setzen dafür einen Breakpoint auf die Zeile 11 auf die Ausgabe der Warnung.

Dann wird der Code in einer speziellen Debugumgebung ausgeführt, die es erlaub die Ausführung zu unterbrechen. In unserm Notebook in VSCode starten wird diese durch das Symbol ../_images/debug_vscode_2.png.

def division(zaehler, nenner):
    log.debug(f"Eingabe Zaehler: {zaehler}")
    log.debug(f"Eingabe Nenner: {nenner}")
    if not isinstance(nenner, (int, float)):
        log.error(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")
    elif not isinstance(zaehler, (int, float)):
        log.error(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")
    elif not nenner:
        log.warning("Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        log.info(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis

division(10, 0)
2025-08-17 14:13:15,972  meinlog  WARNING: Division durch 0

Dies startet den Debug-Modus. In diesem wird die aktuelle Zeile vorgehoben als auch die aktuellen Variablen im Speicher angezeigt.

Debuggin in VS Code

In der Debugumgebung kann man dann Zeile für Zeile vorgehen durch drücken von ../_images/debug_vscode_2.png und damit nachvollziehen wie das Programm bearbeitet wird und welche Variablen sich ändern.

Quiz#

--- shuffleQuestions: true shuffleAnswers: true --- ### Was ist eine Exception in Python? - [x] Eine Ausnahme, die den normalen Ablauf des Programms unterbricht - [ ] Ein Spezialfall bei Loops - [ ] Eine Warnung, die ignoriert werden kann - [ ] Eine globale Variable ### Warum verwendet man `try-except`-Blöcke? - [x] Um kontrolliert auf Fehler zu reagieren - [x] Um das Programm vor Abstürzen zu bewahren - [ ] Um Variablen zu initialisieren - [ ] Um Funktionen automatisch zu wiederholen ### Was passiert, wenn eine Exception im `try`-Block auftritt? - [x] Der Code im `except`-Block wird ausgeführt - [ ] Der `try`-Block wird ignoriert - [ ] Das Programm wird automatisch beendet - [ ] Der Fehler wird immer ignoriert ### Sortiere die folgenden Zeilen, um einen vollständigen `try-except`-Block korrekt aufzubauen: ```python # Annahme: Fehlerhafte Division ``` 1. `try:` 2. ` ergebnis = 10 / 0` 3. ` print("Ergebnis:", ergebnis)` 4. `except:` 5. ` print("Fehler bei der Division")` ### Welcher Fehler steckt in diesem Beispiel? ```python x = 10 y = 0 z = x / y print("Ergebnis:", z) ``` - [x] Division durch null führt zu einer `ZeroDivisionError`-Exception - [ ] `print` darf nicht in try-Blöcken verwendet werden - [ ] `x` darf nicht 10 sein - [ ] `y` muss als String deklariert werden ### Was ist der Vorteil, wenn man `Exception as e` verwendet? - [x] Man kann Typ und Fehlermeldung der Exception gezielt ausgeben - [ ] Dadurch wird die Exception automatisch gelöst - [ ] Der Code wird kürzer - [ ] `as` ist Pflicht in jeder Fehlerbehandlung ### Warum sollte man bekannte Exception-Typen (wie `TypeError`) explizit abfangen? - [x] Um unterschiedliche Fehler gezielt zu behandeln - [ ] Weil Python sonst nicht funktioniert - [ ] Damit die Ausgabe schöner wird - [ ] Das ist nur bei `int`-Werten nötig ### Sortiere die folgenden Zeilen, um eine typisierte Fehlerbehandlung für Divisionen korrekt aufzubauen: ```python # Beispiel mit Division durch 0 und falschem Typ ``` 1. `try:` 2. ` ergebnis = "zehn" / 0` 3. `except TypeError:` 4. ` print("Falscher Datentyp")` 5. `except ZeroDivisionError:` 6. ` print("Division durch 0")` ### Wann wird der `else`-Block bei `try-except-else` ausgeführt? - [x] Nur wenn im `try`-Block **keine** Exception auftritt - [ ] Wenn irgendein Fehler auftritt - [ ] Immer nach dem `except` - [ ] Wenn `print()` im `try` verwendet wird ### Was bewirkt ein `finally`-Block in Python? - [x] Der Code im `finally` wird **immer** ausgeführt – egal ob Fehler oder nicht - [ ] Der Code wird übersprungen bei Fehlern - [ ] Nur verwendet bei Netzwerken - [ ] Dient zur Initialisierung von Variablen ### Wann sollte man eigene Exceptions mit `raise` erzeugen? - [x] Wenn man Fehler gezielt kennzeichnen und weiterreichen will - [ ] Wenn man eine Schleife abbrechen will - [ ] Nur bei Syntaxfehlern - [ ] Nur für Debugging-Zwecke ### Warum ist es sinnvoll, spezifische Fehlertypen wie `ValueError` zu verwenden? - [x] Damit Fehler gezielter behandelt und unterschieden werden können - [ ] Damit `try`-Blöcke kürzer werden - [ ] Damit das Programm schneller läuft - [ ] Weil `Exception` in Python nicht erlaubt ist ### Welcher Fehler steckt in diesem Beispiel? ```python if not isinstance(x, int): raise "Fehler" ``` - [x] `raise` erwartet ein Exception-Objekt, kein String - [ ] `raise` ist kein gültiges Python-Schlüsselwort - [ ] Strings dürfen nicht mit `if` verglichen werden - [ ] `int` ist kein gültiger Typ ### Sortiere die folgenden Zeilen, um eine sichere Divisionsfunktion mit Fehlern zu definieren: ```python # Annahme: Division mit Typprüfung und Division durch 0 ``` 1. `def division(zaehler, nenner):` 2. ` if not isinstance(zaehler, (int, float)):` 3. ` raise ValueError("Zaehler ist ungültig")` 4. ` if nenner == 0:` 5. ` return None` 6. ` return zaehler / nenner` ### Wie verhalten sich Exceptions in einem Funktionsaufruf-Stack? - [x] Sie steigen nach oben, bis sie behandelt oder zum Absturz führen - [ ] Sie verschwinden nach dem ersten Funktionsaufruf - [ ] Sie führen nur in der aktuellen Funktion zum Fehler - [ ] Sie werden automatisch ignoriert ### Was bewirkt folgende Zeile in einer rekursiven Funktion? ```python raise RecursionError("Recursion zu tief") ``` - [x] Sie erzeugt gezielt einen Fehler, wenn die Rekursion zu tief wird - [ ] Sie verhindert eine Exception - [ ] Sie beendet die Rekursion erfolgreich - [ ] Sie wandelt den Rückgabewert in eine Exception um ### Was ist der Stack? - [x] Eine Datenstruktur, die Funktionsaufrufe speichert - [ ] Eine Liste aller Variablen im Programm - [ ] Ein spezieller Datentyp für Schleifen - [ ] Eine Art von Exception ### Was ist der Heap? - [x] Ein Speicherbereich für dynamisch erzeugte Objekte - [ ] Eine spezielle Art von Schleife - [ ] Ein Datentyp für Zahlen - [ ] Ein Bereich für globale Variablen ### Was ist der Unterschied zwischen Stack und Heap? - [x] Stack speichert Funktionsaufrufe, Heap dynamisch erzeugte Objekte - [ ] Stack ist schneller, Heap langsamer - [ ] Stack ist für Schleifen, Heap für Bedingungen - [ ] Stack ist für globale Variablen, Heap für lokale