25. Ausnahmen (Exceptions) souverän behandeln
In Kapitel 24 haben wir gelernt, wie man Ausnahmen (exceptions) liest und versteht, wenn sie auftreten. Jetzt lernen wir, wie wir Ausnahmen behandeln können, sodass sich unsere Programme von Fehlern erholen, statt abzustürzen. Das ist entscheidend, um robuste, benutzerfreundliche Programme zu schreiben, die mit unerwarteten Situationen umgehen können.
Wenn in Python eine Ausnahme auftritt, stoppt der normale Ablauf des Programms sofort. Aber was wäre, wenn wir diese Ausnahme abfangen könnten, bevor sie unser Programm zum Absturz bringt? Was wäre, wenn wir auf den Fehler reagieren könnten, vielleicht indem wir den Benutzer bitten, es erneut zu versuchen, oder indem wir einen Standardwert verwenden, oder indem wir das Problem protokollieren und fortfahren? Genau das ermöglicht uns die Ausnahmebehandlung.
25.1) try- und except-Blöcke verwenden
25.1.1) Die Grundstruktur von try und except
Ein try-except-Block ist Pythons Art zu sagen: „Versuche das zu tun, und wenn eine Ausnahme auftritt, mache stattdessen das.“ Die Grundstruktur sieht so aus:
try:
# Code, der eine Ausnahme auslösen könnte
risky_operation()
except:
# Code, der ausgeführt wird, wenn IRGENDEINE Ausnahme auftritt
print("Something went wrong!")Der try-Block enthält Code, der eine Ausnahme auslösen könnte. Wenn irgendwo im try-Block eine Ausnahme auftritt, stoppt Python sofort die Ausführung des try-Blocks und springt in den except-Block. Tritt keine Ausnahme auf, wird der except-Block vollständig übersprungen.
Schauen wir uns ein konkretes Beispiel an. Erinnern Sie sich aus Kapitel 24 daran, dass der Versuch, einen ungültigen String in eine Ganzzahl umzuwandeln, eine ValueError auslöst:
# Ohne Ausnahmebehandlung – Programm stürzt ab
user_input = "hello"
number = int(user_input) # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")Jetzt behandeln wir diese Ausnahme souverän:
# Mit Ausnahmebehandlung – Programm läuft weiter
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except:
print("That's not a valid number!")
number = 0 # Einen Standardwert verwenden
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0Das Programm ist nicht abgestürzt! Als int(user_input) einen ValueError auslöste, sprang Python in den except-Block, gab unsere Fehlermeldung aus, setzte einen Standardwert und fuhr dann mit dem restlichen Programm fort.
Das passiert Schritt für Schritt:
Den „Sprung“ verstehen – was wirklich passiert
Wenn wir sagen, Python „springt“ in den except-Block, meinen wir, dass es die normale sequenzielle Ausführung aufgibt. Das ist eine grundlegende Änderung darin, wie Ihr Programm abläuft – nicht nur ein einfacher Zweig wie bei einer if-Anweisung. Sehen wir uns das im Detail mit einem konkreten Beispiel an:
# Ausführungsfluss mit Ausnahmen beobachten
print("1. Starting program")
try:
print("2. Entered try block")
number = int("hello") # Ausnahme passiert HIER
print("3. After conversion") # Diese Zeile wird NIE ausgeführt
result = number * 2 # Diese Zeile wird NIE ausgeführt
print("4. After calculation") # Diese Zeile wird NIE ausgeführt
except ValueError:
print("5. In except block - handling the error")
print("6. After try-except block")Output:
1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except blockBeachten Sie, dass die Zeilen 3 und 4 nie ausgeführt werden! In dem Moment, in dem int("hello") einen ValueError auslöst, macht Python Folgendes:
- Stoppt die Ausführung des try-Blocks sofort – genau in der Zeile, in der die Ausnahme auftrat
- Sucht nach einer passenden except-Klausel, die diesen Ausnahmetyp behandeln kann
- Springt in diesen except-Block und überspringt den restlichen Code im try-Block
- Fährt fort nach der try-except-Struktur, sobald der except-Block abgeschlossen ist
Das unterscheidet sich grundlegend vom normalen Programmablauf. Bei normaler Ausführung führt Python jede Zeile nacheinander aus. Bei einer Ausnahme verlässt Python den aktuellen Pfad und nimmt eine völlig andere Route. Ohne Ausnahmebehandlung würde das Programm in Zeile 2 abstürzen und beendet werden. Mit Ausnahmebehandlung erholt sich das Programm und läuft weiter.
Warum das wichtig ist:
Dieses „Sprung“-Verhalten zu verstehen, ist entscheidend, weil:
- Jeglicher Code nach der Ausnahme im try-Block wird übersprungen – Sie können nicht davon ausgehen, dass spätere Zeilen im try-Block ausgeführt wurden
- Variablen sind möglicherweise nicht initialisiert, wenn die Ausnahme vor ihrer Zuweisung auftritt
- Sie müssen planen, in welchem Zustand Ihr Programm ist, wenn der except-Block ausgeführt wird
25.1.2) Benutzereingaben sicher behandeln
Eine der häufigsten Anwendungen der Ausnahmebehandlung ist die Validierung von Benutzereingaben. Benutzer können alles Mögliche eintippen, und wir müssen ungültige Eingaben souverän behandeln. Hier ist ein praktisches Beispiel für ein Programm, das nach dem Alter eines Benutzers fragt:
# Sichere Alterseingabe mit Ausnahmebehandlung
print("Please enter your age:")
user_input = input()
try:
age = int(user_input)
print(f"You are {age} years old.")
# Geburtsjahr berechnen (angenommen, das aktuelle Jahr ist 2024)
birth_year = 2024 - age
print(f"You were born around {birth_year}.")
except:
print("Invalid input! Age must be a number.")
print("Using default age of 0.")
age = 0Wenn der Benutzer „25“ eingibt, ist die Ausgabe:
Please enter your age:
25
You are 25 years old.
You were born around 1999.Wenn der Benutzer „twenty-five“ eingibt, ist die Ausgabe:
Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.Beachten Sie, wie das Programm den Fehler souverän behandelt, statt mit einem Traceback abzustürzen. Das ist viel besser für die Benutzererfahrung.
25.1.3) Mehrere Operationen in einem try-Block behandeln
Sie können mehrere Operationen in einen einzelnen try-Block setzen. Wenn eine davon eine Ausnahme auslöst, springt Python sofort in den except-Block. Beginnen wir mit einem einfachen Beispiel:
# Zwei Operationen im try-Block
print("Enter a number:")
user_input = input()
try:
number = int(user_input) # Erste Operation – könnte ValueError auslösen
result = 100 / number # Zweite Operation – könnte ZeroDivisionError auslösen
print(f"100 / {number} = {result}")
except:
print("Something went wrong!")Wenn der Benutzer „hello“ eingibt, tritt die Ausnahme bei der ersten Operation (Umwandlung) auf. Wenn der Benutzer „0“ eingibt, tritt die Ausnahme bei der zweiten Operation (Division) auf. In beiden Fällen fängt unser einzelner except-Block sie ab.
Jetzt erweitern wir das auf drei Operationen:
# Mehrere Operationen im try-Block
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input) # Könnte ValueError auslösen
denominator = int(denominator_input) # Könnte ValueError auslösen
result = numerator / denominator # Könnte ZeroDivisionError auslösen
print(f"{numerator} / {denominator} = {result}")
except:
print("Something went wrong with the calculation!")
print("Make sure you enter valid numbers and don't divide by zero.")Wenn der Benutzer „10“ und „2“ eingibt:
Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0Wenn der Benutzer „10“ und „zero“ eingibt:
Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.Wenn der Benutzer „10“ und „0“ eingibt:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.In diesem Beispiel könnten drei verschiedene Dinge schiefgehen: Die Umwandlung des Zählers könnte fehlschlagen, die Umwandlung des Nenners könnte fehlschlagen oder die Division könnte fehlschlagen (wenn der Nenner null ist). Unser einzelner except-Block fängt alle diese Fälle ab. Allerdings hat dieser Ansatz eine Einschränkung: Wir können nicht erkennen, welcher konkrete Fehler aufgetreten ist. Darum kümmern wir uns im nächsten Abschnitt.
25.1.4) Das Problem mit nackten except-Klauseln
Die Verwendung von except: ohne Angabe eines Ausnahmetyps nennt man eine nackte except-Klausel (bare except clause). Sie fängt zwar alle Ausnahmen ab, ist aber oft zu breit und kann unerwartete Probleme verbergen. Betrachten Sie dieses Beispiel:
# Nacktes except fängt ALLES ab – sogar Dinge, die wir nicht erwarten
numbers = [10, 20, 30]
try:
index = 5 # Wir erwarten IndexError, wenn index außerhalb des Bereichs liegt
value = numbers[index]
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Das wirkt sinnvoll – wir versuchen, auf ein Listenelement zuzugreifen, das möglicherweise nicht existiert. Aber was ist, wenn ein Tippfehler in unserem Code ist?
# Was, wenn ein Tippfehler in unserem Code ist?
numbers = [10, 20, 30]
try:
index = 2
value = numbrs[index] # Tippfehler: 'numbrs' statt 'numbers'
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Output:
Could not access the list element.Das nackte except fängt den NameError vom Tippfehler ab und gibt „Could not access the list element“ aus – und liefert uns damit die falsche Information darüber, was schiefgelaufen ist! Wir denken, der Index sei außerhalb des Bereichs, aber tatsächlich haben wir einen Tippfehler im Variablennamen.
Ein nacktes except fängt außerdem KeyboardInterrupt (wenn der Benutzer Strg+C drückt) und SystemExit (wenn das Programm versucht zu beenden) ab, was normalerweise nicht abgefangen werden sollte. Aus diesen Gründen ist es besser, spezifische Ausnahmen abzufangen, was wir als Nächstes lernen.
25.2) Spezifische Ausnahmen abfangen
25.2.1) Ausnahmetypen angeben
Statt alle Ausnahmen mit einem nackten except abzufangen, können wir angeben, welche Ausnahmetypen wir behandeln wollen. Das macht unseren Code präziser und hilft uns, angemessen auf unterschiedliche Fehler zu reagieren:
# Einen spezifischen Ausnahmetyp abfangen
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except ValueError:
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0Jetzt fängt unsere except-Klausel nur ValueError-Ausnahmen ab. Wenn eine andere Art von Ausnahme auftritt (wie ein NameError durch einen Tippfehler), wird sie nicht abgefangen, und wir sehen den vollständigen Traceback – was beim Debugging tatsächlich hilfreich ist!
Die Syntax ist: except ExceptionType: wobei ExceptionType der Name der Ausnahme-Klasse ist, die Sie abfangen möchten (wie ValueError, TypeError, ZeroDivisionError usw.).
Häufiger Fehler: Den falschen Ausnahmetyp abfangen
Was passiert, wenn Sie einen Ausnahmetyp angeben, der nicht zu dem passt, was tatsächlich auftritt? Schauen wir uns das an:
# Den falschen Ausnahmetyp abfangen
user_input = "hello"
try:
number = int(user_input) # Das löst ValueError aus
print(f"You entered: {number}")
except TypeError: # Aber wir fangen TypeError ab!
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
Traceback (most recent call last):
File "example.py", line 4, in <module>
number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'Das Programm ist abgestürzt! Warum? Weil int("hello") einen ValueError auslöst, aber unsere except-Klausel nur TypeError abfängt. Da es keine passende except-Klausel gibt, wird die Ausnahme nicht abgefangen und das Programm beendet sich.
Das ist während der Entwicklung tatsächlich hilfreich – wenn Sie den falschen Ausnahmetyp abfangen, sehen Sie den vollständigen Traceback und erkennen Ihren Fehler. Das ist ein Grund, warum das Abfangen spezifischer Ausnahmen besser ist als ein nacktes except.
So vermeiden Sie diesen Fehler:
- Lesen Sie den Traceback, um zu sehen, welcher Ausnahmetyp tatsächlich aufgetreten ist
- Verwenden Sie diesen spezifischen Ausnahmetyp in Ihrer except-Klausel
- Wenn Sie unsicher sind, lassen Sie den Code laufen und abstürzen – der Traceback wird es Ihnen sagen!
25.2.2) Unterschiedliche Ausnahmen unterschiedlich behandeln
Sie können mehrere except-Klauseln haben, um unterschiedliche Ausnahmetypen auf unterschiedliche Weise zu behandeln. Das ist extrem nützlich, wenn verschiedene Fehler unterschiedliche Reaktionen erfordern:
# Unterschiedliche Behandlung für unterschiedliche Ausnahmen
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input)
denominator = int(denominator_input)
result = numerator / denominator
print(f"{numerator} / {denominator} = {result}")
except ValueError:
print("Error: Both inputs must be valid integers.")
print("You entered something that isn't a number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
print("The denominator must be a non-zero number.")Wenn der Benutzer „10“ und „abc“ eingibt:
Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.Wenn der Benutzer „10“ und „0“ eingibt:
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.Python prüft jede except-Klausel der Reihe nach. Wenn eine Ausnahme auftritt, findet Python die erste except-Klausel, die zum Ausnahmetyp passt, und führt diesen Block aus. Die anderen except-Klauseln werden übersprungen.
25.2.3) Mehrere Ausnahmetypen in einer Klausel abfangen
Manchmal möchten Sie mehrere verschiedene Ausnahmetypen auf die gleiche Weise behandeln. Statt mehrere identische except-Blöcke zu schreiben, können Sie mehrere Ausnahmetypen in einer einzigen Klausel abfangen, indem Sie sie in Klammern als Tuple angeben:
# Mehrere Ausnahmetypen zusammen abfangen
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
print("Please enter a non-zero number.")Wenn der Benutzer „hello“ eingibt:
Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.Wenn der Benutzer „0“ eingibt:
Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.Sowohl ValueError (durch ungültige Umwandlung) als auch ZeroDivisionError (durch Division durch Null) werden durch dieselbe except-Klausel behandelt. Das ist nützlich, wenn unterschiedliche Fehler dieselbe Reaktion auslösen sollen.
25.2.4) Auf Ausnahmeinformationen zugreifen
Manchmal müssen Sie mehr Details über die aufgetretene Ausnahme wissen. Sie können das Ausnahmeobjekt mit dem Schlüsselwort as abfangen. Aber zuerst sollten wir verstehen, was ein Ausnahmeobjekt überhaupt ist.
Was ist ein Ausnahmeobjekt?
Wenn Python eine Ausnahme auslöst, signalisiert es nicht nur, dass etwas schiefgelaufen ist – es erstellt ein Objekt, das Informationen über den Fehler enthält. Dieses Ausnahmeobjekt ist wie ein detaillierter Fehlerbericht, der Folgendes enthält:
- Die Fehlermeldung: Eine Beschreibung dessen, was schiefgelaufen ist
- Den Ausnahmetyp: Welche Art von Fehler aufgetreten ist (ValueError, TypeError, usw.)
- Zusätzliche Attribute: Spezifische Informationen abhängig vom Ausnahmetyp
Stellen Sie sich ein Ausnahmeobjekt als Container vor, der alle Informationen über einen Fehler enthält. So wie ein Listenobjekt Elemente enthält und Methoden wie append() hat, enthält ein Ausnahmeobjekt Fehlerinformationen und hat Attribute, auf die Sie zugreifen können.
Wenn Sie except ValueError as error: schreiben, sagen Sie Python: „Wenn ein ValueError auftritt, erstelle eine Variable namens error und lege das Ausnahmeobjekt hinein, damit ich es untersuchen kann.“
Lassen Sie uns erkunden, was in einem Ausnahmeobjekt steckt:
# Inhalte des Ausnahmeobjekts untersuchen
try:
number = int("hello")
except ValueError as error:
print("Exception caught! Let's examine it:")
print(f"Type: {type(error)}")
print(f"String representation: {error}")
print(f"Args tuple: {error.args}")Output:
Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)Das Ausnahmeobjekt hat:
- Einen Typ (ValueError-Klasse) – das sagt Ihnen, welche Art von Fehler aufgetreten ist
- Eine String-Repräsentation (die Fehlermeldung) – das ist, was Sie in Tracebacks sehen
- Ein args-Attribut (Tuple, das die Meldung und ggf. andere Argumente enthält) – das liefert strukturierten Zugriff auf Fehlerdetails
Warum das wichtig ist:
Verschiedene Ausnahmetypen haben unterschiedliche Attribute, die spezifische Informationen liefern. Wenn Sie die Struktur von Ausnahmeobjekten verstehen, können Sie nützliche Informationen für Debugging oder Benutzerfeedback extrahieren:
# Unterschiedliche Ausnahmen haben unterschiedliche Attribute
numbers = [10, 20, 30]
try:
value = numbers[10]
except IndexError as error:
print(f"IndexError message: {error}")
print(f"Exception args: {error.args}")
# Jetzt mit einem Dictionary versuchen
grades = {"Alice": 95}
try:
grade = grades["Bob"]
except KeyError as error:
print(f"KeyError message: {error}")
print(f"Missing key: {error.args[0]}")Output:
IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: BobBeachten Sie, wie KeyError in seiner Meldung den tatsächlich fehlenden Schlüssel enthält. Unterschiedliche Ausnahmetypen liefern unterschiedliche nützliche Informationen, auf die Sie über das Ausnahmeobjekt zugreifen können.
25.3) else und finally mit try-Blöcken verwenden
25.3.1) Die else-Klausel: Code, der nur bei Erfolg ausgeführt wird
Die else-Klausel in einem try-except-Block läuft nur, wenn keine Ausnahme aufgetreten ist im try-Block. Das ist nützlich für Code, der nur ausgeführt werden soll, wenn die riskante Operation erfolgreich ist:
# else für Code verwenden, der nur bei Erfolg läuft
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
except ValueError:
print("That's not a valid number!")
else:
# Das läuft nur, wenn int(user_input) erfolgreich war
print(f"Successfully converted: {number}")
squared = number ** 2
print(f"The square of {number} is {squared}")Wenn der Benutzer „5“ eingibt:
Enter a number:
5
Successfully converted: 5
The square of 5 is 25Wenn der Benutzer „hello“ eingibt:
Enter a number:
hello
That's not a valid number!Warum else verwenden, statt den Code einfach ans Ende des try-Blocks zu setzen? Es gibt zwei wichtige Gründe:
- Klarheit: Die
else-Klausel macht explizit, dass dieser Code nur bei Erfolg ausgeführt wird - Ausnahme-Scope: Ausnahmen, die in der
else-Klausel ausgelöst werden, werden nicht von den vorhergehendenexcept-Klauseln abgefangen
Hier ist ein Beispiel, das zeigt, warum der zweite Punkt wichtig ist:
# Demonstration, warum else für den Ausnahme-Scope nützlich ist
try:
number_1 = int(input("Enter a number_1: "))
except ValueError:
print("Invalid input!")
else:
# Wenn hier ein Fehler auftritt, wird er nicht vom except oben abgefangen
# Das hilft, zwischen Eingabefehlern und Verarbeitungsfehlern zu unterscheiden
number_2 = int(input("Enter a number_2: ")) # Could raise ValueErrorWenn wir number_2 = int(input(...)) zusammen mit number_1 in den try-Block setzen würden, würde jeder ValueError von beiden Eingaben durch dieselbe except ValueError-Klausel abgefangen werden. Damit wäre es unmöglich zu erkennen, welche Eingabe das Problem verursacht hat.
Indem wir number_2 = int(input(...)) in den else-Block setzen, trennen wir die Fehlerbehandlung. Die except-Klausel fängt nur Fehler von number_1 ab, während Fehler von number_2 eine nicht abgefangene Ausnahme mit vollständigem Traceback auslösen – wodurch klar ist, dass die zweite Eingabe fehlgeschlagen ist, nicht die erste.
25.3.2) Die finally-Klausel: Code, der immer ausgeführt wird
Die finally-Klausel enthält Code, der in jedem Fall ausgeführt wird – egal, ob eine Ausnahme aufgetreten ist oder nicht, egal, ob sie abgefangen wurde oder nicht. Das ist entscheidend für Aufräumarbeiten, die immer passieren müssen:
# finally für Aufräumarbeiten verwenden
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("Invalid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
finally:
print("Calculation attempt completed.")Wenn der Benutzer „5“ eingibt:
Enter a number:
5
Result: 20.0
Calculation attempt completed.Wenn der Benutzer „hello“ eingibt:
Enter a number:
hello
Invalid number!
Calculation attempt completed.Wenn der Benutzer „0“ eingibt:
Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.Der finally-Block läuft in allen drei Fällen! Das ist das zentrale Verhalten von finally: Es wird immer ausgeführt, unabhängig davon, was im try-Block passiert ist.
25.3.3) try, except, else und finally kombinieren
Sie können alle vier Klauseln zusammen verwenden, um eine umfassende Ausnahmebehandlung zu erstellen:
# Vollständige Struktur zur Ausnahmebehandlung
print("Enter a number to calculate its reciprocal:")
user_input = input()
try:
# Riskante Operationen
number = int(user_input)
reciprocal = 1 / number
except ValueError:
# Umwandlungsfehler behandeln
print("Error: Input must be a valid integer.")
except ZeroDivisionError:
# Division durch Null behandeln
print("Error: Cannot calculate reciprocal of zero.")
else:
# Code nur bei Erfolg
print(f"The reciprocal of {number} is {reciprocal}")
print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
# Aufräumcode, der immer läuft
print("Reciprocal calculation completed.")Wenn der Benutzer „4“ eingibt:
Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.Wenn der Benutzer „hello“ eingibt:
Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.Wenn der Benutzer „0“ eingibt:
Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.Der Ausführungsablauf ist:
- Der
try-Block wird immer zuerst ausgeführt - Wenn eine Ausnahme auftritt, wird der passende
except-Block ausgeführt - Wenn keine Ausnahme auftritt, wird der
else-Block ausgeführt (falls vorhanden) - Der
finally-Block wird immer zuletzt ausgeführt, unabhängig davon, was passiert ist
25.4) Ausnahmen gezielt mit raise auslösen
25.4.1) Warum Ausnahmen auslösen?
Bisher haben wir Ausnahmen abgefangen, die Python automatisch auslöst. Aber manchmal müssen Sie in Ihrem eigenen Code bewusst eine Ausnahme auslösen. Das ist nützlich, wenn:
- Sie eine ungültige Situation erkennen, die Ihr Code nicht behandeln kann
- Sie Regeln oder Einschränkungen erzwingen möchten
- Sie einen Fehler an Code signalisieren möchten, der Ihre Funktion aufgerufen hat
Eine Ausnahme auszulösen ist Pythons Art zu sagen: „Ich kann nicht weitermachen – etwas ist falsch, und wer mich aufgerufen hat, muss sich darum kümmern.“
Die Syntax ist einfach: raise ExceptionType("error message")
Hier ist ein grundlegendes Beispiel:
# Eine Ausnahme gezielt auslösen
age = -5
if age < 0:
raise ValueError("Age cannot be negative!")
print(f"Age: {age}") # Diese Zeile wird nie ausgeführtOutput:
Traceback (most recent call last):
File "example.py", line 5, in <module>
raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!Wenn Python auf raise trifft, erstellt es sofort eine Ausnahme und beginnt nach einem except-Block zu suchen, der sie behandeln kann. Gibt es keinen, wird das Programm mit einem Traceback beendet.
25.4.2) Ausnahmen in Funktionen auslösen
Ausnahmen auszulösen ist besonders in Funktionen nützlich, um Eingaben zu validieren und Einschränkungen durchzusetzen:
# Funktion, die Eingaben durch das Auslösen von Ausnahmen validiert
def calculate_discount(price, discount_percent):
"""Calculate discounted price.
Args:
price: Original price (must be positive)
discount_percent: Discount percentage (must be 0-100)
Returns:
Discounted price
Raises:
ValueError: If inputs are invalid
"""
if price < 0:
raise ValueError("Price cannot be negative!")
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100!")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Die Funktion verwenden
try:
final_price = calculate_discount(100, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Final price: $80.0Jetzt probieren wir es mit ungültigen Eingaben:
# Ungültiger Preis
try:
final_price = calculate_discount(-50, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Price cannot be negative!# Ungültiger Rabatt
try:
final_price = calculate_discount(100, 150)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Discount must be between 0 and 100!Indem die Funktion Ausnahmen auslöst, kommuniziert sie klar, was schiefgelaufen ist. Der aufrufende Code kann dann entscheiden, wie er den Fehler behandelt – vielleicht indem er den Benutzer nach einer neuen Eingabe fragt, Standardwerte verwendet oder den Fehler protokolliert.
25.4.3) Den richtigen Ausnahmetyp wählen
Python hat viele eingebaute Ausnahmetypen, und den richtigen zu wählen macht Ihren Code verständlicher. Hier sind die am häufigsten verwendeten Ausnahmen für Validierung:
- ValueError: Verwenden Sie diese, wenn ein Wert den richtigen Typ hat, aber einen unpassenden Wert (z. B. negatives Alter, ungültiger Prozentsatz)
- TypeError: Verwenden Sie diese, wenn ein Wert ein komplett falscher Typ ist (z. B. String statt Zahl)
- KeyError: Verwenden Sie diese, wenn ein Dictionary-Schlüssel nicht existiert
- IndexError: Verwenden Sie diese, wenn ein Sequenzindex außerhalb des Bereichs liegt
Hier ist ein Beispiel, das verschiedene Ausnahmetypen zeigt:
# Geeignete Ausnahmetypen verwenden
def get_student_grade(grades, student_name):
"""Get a student's grade from the grades dictionary.
Args:
grades: Dictionary mapping student names to grades
student_name: Name of the student
Returns:
The student's grade
Raises:
TypeError: If grades is not a dictionary
KeyError: If student_name is not in grades
ValueError: If the grade is invalid
"""
if not isinstance(grades, dict):
raise TypeError("Grades must be a dictionary!")
if student_name not in grades:
raise KeyError(f"Student '{student_name}' not found!")
grade = grades[student_name]
if not (0 <= grade <= 100):
raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
return grade
# Test mit gültigen Daten
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
try:
grade = get_student_grade(grades, "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Alice's grade: 95# Test mit fehlendem Studenten
try:
grade = get_student_grade(grades, "David")
print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Student 'David' not found!# Test mit falschem Typ
try:
grade = get_student_grade("not a dict", "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Grades must be a dictionary!Die Verwendung des passenden Ausnahmetyps hilft anderen Programmierern (und Ihrem zukünftigen Ich) zu verstehen, welche Art von Fehler aufgetreten ist.
25.4.4) Ausnahmen erneut auslösen
Manchmal möchten Sie eine Ausnahme abfangen, etwas tun (wie Logging) und dann die Ausnahme weiter propagieren lassen. Das können Sie tun, indem Sie raise ohne Argumente innerhalb eines except-Blocks verwenden:
# Eine Ausnahme nach dem Logging erneut auslösen
def divide_numbers(a, b):
"""Divide two numbers with error logging."""
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR LOG: Division by zero attempted")
print(f" Numerator: {a}, Denominator: {b}")
raise # Dieselbe Ausnahme erneut auslösen
# Die Funktion verwenden
try:
result = divide_numbers(10, 0)
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")Output:
ERROR LOG: Division by zero attempted
Numerator: 10, Denominator: 0
Cannot divide by zero!Die raise-Anweisung ohne Argumente löst die Ausnahme, die gerade abgefangen wurde, erneut aus. Das ist nützlich, wenn Sie:
- Den Fehler protokollieren oder aufzeichnen wollen
- Aufräumarbeiten durchführen wollen
- Den Fehler an den Aufrufer weitergeben wollen
25.4.5) Ausnahmen aus Ausnahmen auslösen
Manchmal möchten Sie beim Behandeln einer Ausnahme eine neue Ausnahme auslösen und dabei den Kontext des ursprünglichen Fehlers bewahren. Python 3 bietet dafür die Syntax raise ... from ...:
# Eine neue Ausnahme aus einer bestehenden auslösen
def load_config(config_dict, key):
"""Load configuration value from dictionary."""
try:
config_value = config_dict[key]
# Versuchen, als Integer zu parsen
parsed_value = int(config_value)
return parsed_value
except KeyError as error:
raise RuntimeError(f"Configuration key missing: {key}") from error
except ValueError as error:
raise RuntimeError(f"Invalid configuration format for {key}") from error
# Die Funktion verwenden
config = {"timeout": "30", "retries": "5"}
try:
value = load_config(config, "timeout")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Config value: 30Wenn der Schlüssel nicht existiert:
try:
value = load_config(config, "missing_key")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'Das Schlüsselwort from verknüpft die neue Ausnahme mit der ursprünglichen. Dadurch entsteht eine Kette von Ausnahmen, die beim Debugging hilft – Sie können sowohl sehen, was auf hoher Ebene schiefgelaufen ist (Konfigurationsfehler) als auch, was die zugrunde liegende Ursache war (Schlüssel nicht gefunden).
Ausnahmebehandlung ist eines der wichtigsten Werkzeuge, um zuverlässige Programme zu schreiben. Mit try-except-Blöcken können Sie Probleme vorwegnehmen, sie souverän behandeln und Ihren Benutzern eine bessere Erfahrung bieten. Denken Sie daran:
- Verwenden Sie
try-except, um erwartete Fehler souverän zu behandeln - Fangen Sie spezifische Ausnahmetypen ab, statt ein nacktes
exceptzu verwenden - Verwenden Sie
elsefür Code, der nur bei Erfolg laufen soll - Verwenden Sie
finallyfür Aufräumcode, der immer laufen muss - Lösen Sie in Ihrem eigenen Code Ausnahmen aus, um Probleme zu signalisieren
- Wählen Sie passende Ausnahmetypen, um Fehler klar zu machen
- Geben Sie hilfreiche Fehlermeldungen aus, die erklären, was schiefgelaufen ist
Im nächsten Kapitel lernen wir Techniken der defensiven Programmierung kennen, die Ausnahmebehandlung mit Eingabevalidierung und anderen Strategien kombinieren, um unsere Programme noch robuster zu machen.