Zum Inhalt
wannacode

Werfen und eigene Exceptions

Kurz gesagt: Mit raise lehnt eine Funktion ab und wirft eine Exception, die durch die Aufrufe fällt, bis ein except sie fängt; für deinen eigenen Fall schreibst du eine eigene Exception-Klasse.

Problem

Der Geldautomat sagt „Nimm dein Geld“

Das Konto speichert seinen Kontostand. Wenn der Betrag zum Abheben negativ ist oder das Geld nicht reicht, tut die Methode einfach nichts:

def withdraw(self, amount):
if amount > 0 and amount <= self.balance:
self.balance -= amount

Der Geldautomat benutzt sie so:

account = Account(100)
account.withdraw(500)
print("Nimm dein Geld.")
print(f"Kontostand: {account.balance} €")
Ausgabe
Nimm dein Geld.
Kontostand: 100 €

Der Kunde will 500 € und hat 100 €. Die Methode lehnt still ab, der Geldautomat merkt nichts und sagt „Nimm dein Geld.“ Der Fehler geht verloren. Die Methode könnte True oder False zurückgeben, aber der Aufrufer vergisst leicht, das zu prüfen, und False sagt nicht, warum.

Die Idee

raise: Ich lehne ab und sage, warum

Statt zu schweigen, wirft die Methode eine Exception:

def withdraw(self, amount):
if amount <= 0:
raise ValueError("Der Betrag muss positiv sein")
if amount > self.balance:
raise ValueError("Nicht genug Geld")
self.balance -= amount

raise beendet die Methode wie return. Die Zeile self.balance -= amount wird nie erreicht. Aber statt eines Werts schickt die Methode eine Exception, und der Aufrufer kann sie nicht übersehen:

account = Account(100)
try:
account.withdraw(500)
print("Nimm dein Geld.")
except ValueError as e:
print(f"Abgelehnt: {e}")
print(f"Kontostand: {account.balance} €")
Ausgabe
Abgelehnt: Nicht genug Geld
Kontostand: 100 €
  • ValueError("Nicht genug Geld") erzeugt das Exception-Objekt wie jedes Objekt in Python. Die Meldung kommt in seinen Konstruktor, und print(e) und der f-String f"{e}" geben sie zurück.
  • ValueError ist die fertige Exception für „der Wert des Arguments ist falsch“. Du benutzt sie genau bei solchen Prüfungen.
Genauer, für Neugierige
  • Eine weitere häufige fertige Exception ist TypeError: Der Wert hat den falschen Typ, etwa Text statt einer Zahl. Für eine Aktion, für die das Objekt nicht im richtigen Zustand ist (Abheben von einem geschlossenen Konto), hat Python keine eigene fertige Exception. Dann schreibst du deine eigene, wie du gleich sehen wirst.
  • raise funktioniert nur mit einem Objekt einer Unterklasse von BaseException. Einen String kannst du nicht werfen: raise "Nicht genug Geld" bricht mit TypeError: exceptions must derive from BaseException ab.

Sieh es dir an

Die Exception fällt durch

def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Nicht genug Geld")
self.balance -= amount
def pay_rent(account):
account.withdraw(800)
print("Die Miete ist bezahlt.")
try:
pay_rent(Account(500))
except ValueError as e:
print(f"Abgelehnt: {e}")
Schritt für Schritt, in Worten
  1. Das Programm ruft pay_rent für ein Konto mit 500 € auf.
  2. pay_rent ruft withdraw(800) auf: ein neuer Frame oben drauf.
  3. 800 > 500: Die Methode wirft mit raise eine Exception.
  4. Der Kontostand ändert sich nicht: Der Frame wird verworfen, ohne seine Zeilen zu Ende auszuführen.
  5. pay_rent hat kein except: „Die Miete ist bezahlt.“ wird nicht ausgegeben, und der Frame wird verworfen.
  6. Das except im Programm fängt die Exception, und das Programm macht hier weiter.

Erinnere dich an den Aufrufstapel aus der Lektion über Rekursion. Ganz unten ist das Programm: der Code auf oberster Ebene. Es ruft pay_rent auf, und diese Funktion ruft withdraw auf. Die Frames liegen übereinander.

Wenn withdraw wirft, sucht die Exception ein except, indem sie den Stapel hinuntergeht. Ein Frame ohne passendes except wird sofort verworfen, ohne seine Zeilen zu Ende auszuführen. So geht es auch pay_rent. Erst das Programm hat ein except, und es macht dort weiter.

Gäbe es auch dort kein except, würde das Programm anhalten und einen Stacktrace ausgeben: genau diese Frames.

Genauer, für Neugierige

Hier ist dasselbe Programm, aber niemand fängt (die Datei rent.py):

class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Nicht genug Geld")
self.balance -= amount
def pay_rent(account):
account.withdraw(800)
print("Die Miete ist bezahlt.")
pay_rent(Account(500))
Fehler
Traceback (most recent call last):
File "/projects/rent/rent.py", line 16, in <module>
pay_rent(Account(500))
File "/projects/rent/rent.py", line 12, in pay_rent
account.withdraw(800)
File "/projects/rent/rent.py", line 7, in withdraw
raise ValueError("Nicht genug Geld")
ValueError: Nicht genug Geld

In Python heißt der Stacktrace Traceback, und die Frames stehen von außen nach innen: Ganz oben ist das Programm (<module>), ganz unten withdraw, und die letzte Zeile ist die Art und die Meldung. „most recent call last“ heißt „der letzte Aufruf steht ganz unten“. Fang in Python also unten an zu lesen.

Probier es aus

Was erscheint auf dem Bildschirm?

Das Konto hat 50 €, und die Bestellung kostet 120 €.

Was gibt das Programm aus?

def checkout(account):
print("Ich bezahle die Bestellung")
account.withdraw(120)
print("Die Bestellung ist bezahlt")
account = Account(50)
try:
checkout(account)
print("Ich sende die Bestätigung")
except ValueError as e:
print(f"Abgelehnt: {e}")
print(f"Kontostand: {account.balance} €")
Antwort

Probier es aus

Finde den Fehler

Die Miete beträgt 800 €, und auf dem Konto sind 500 €. Das Programm sagt „Die Miete ist bezahlt.“ In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Genauer, für Neugierige
  • Manchmal fängt eine Funktion, tut etwas, etwa einen Eintrag ins Log schreiben, und wirft dieselbe Exception weiter. In Python ist das raise ohne etwas danach, im except. Der Traceback bleibt der ursprüngliche.
def pay_rent(account):
try:
account.withdraw(800)
except ValueError:
print("Log: Die Miete ist nicht durchgegangen")
raise
try:
pay_rent(Account(500))
except ValueError as e:
print(f"Abgelehnt: {e}")
Ausgabe
Log: Die Miete ist nicht durchgegangen
Abgelehnt: Nicht genug Geld
  • Du kannst auch eine andere Exception werfen, die besser sagt, was passiert ist, und die alte als Ursache behalten. In Python: raise RuntimeError("Die Miete ist nicht bezahlt") from e. Die alte steht in e.__cause__, und der Traceback zeigt beide, mit der Zeile „The above exception was the direct cause of the following exception“.
def pay_rent(account):
try:
account.withdraw(800)
except ValueError as e:
raise RuntimeError("Die Miete ist nicht bezahlt") from e
try:
pay_rent(Account(500))
except RuntimeError as e:
print(f"{e}, weil: {e.__cause__}")
Ausgabe
Die Miete ist nicht bezahlt, weil: Nicht genug Geld

Die Idee

Eine eigene Exception

„Nicht genug Geld“ ist Text. Wenn der Geldautomat „Zahle noch 30 € ein“ vorschlagen will, muss er wissen, wie viel fehlt. Die Zahl aus dem Text herauszuziehen ist zerbrechlich. Schreib lieber eine eigene Exception-Klasse mit einem Feld:

class InsufficientFundsError(Exception):
def __init__(self, shortfall):
super().__init__(f"Es fehlen {shortfall} €")
self.shortfall = shortfall

Deine eigene Exception ist eine gewöhnliche Klasse, die von Exception erbt. Der Konstruktor gibt die Meldung mit super().__init__(...) an die Basisklasse weiter. Die Methode wirft sie, wenn das Geld nicht reicht:

def withdraw(self, amount):
if amount <= 0:
raise ValueError("Der Betrag muss positiv sein")
if amount > self.balance:
raise InsufficientFundsError(amount - self.balance)
self.balance -= amount

Und der Aufrufer fängt die beiden Fälle getrennt:

account = Account(100)
try:
account.withdraw(130)
except InsufficientFundsError as e:
print(e)
print(f"Zahle noch {e.shortfall} € ein oder wähle einen kleineren Betrag.")
except ValueError as e:
print(f"Abgelehnt: {e}")
Ausgabe
Es fehlen 30 €
Zahle noch 30 € ein oder wähle einen kleineren Betrag.
Genauer, für Neugierige
  • Namen von Exceptions enden immer auf Error. So heißen auch die fertigen: ValueError, TypeError. Der Styleguide PEP 8 verlangt das für jede Exception, die ein Fehler ist.
  • Deine eigene Exception kann auch von einer spezifischeren fertigen Klasse erben, etwa ValueError. Dann fängt except ValueError auch sie. Hier ist sie getrennt, deshalb spielt die Reihenfolge der beiden except-Blöcke keine Rolle.

Die Idee

Eigene oder fertige?

Checked Exceptions wie in Java gibt es hier nicht. Niemand prüft vor dem Start, ob der Aufrufer fängt. Was eine Funktion wirft, steht in ihrer Dokumentation: ein Docstring mit einem Abschnitt „Raises:“.

def withdraw(self, amount):
"""Hebt den Betrag vom Konto ab.
Raises:
ValueError: wenn der Betrag nicht positiv ist.
InsufficientFundsError: wenn das Geld nicht reicht.
"""
if amount <= 0:
raise ValueError("Der Betrag muss positiv sein")
if amount > self.balance:
raise InsufficientFundsError(amount - self.balance)
self.balance -= amount

Das ist nur eine Gewohnheit, für Menschen und für den Editor. Ein Programm, das withdraw ohne try aufruft, startet und läuft, bis die Exception wirklich geworfen wird. Die Frage ist eine andere: Brauchst du überhaupt eine eigene Exception?

Eine fertige Exception, wenn die Bedeutung passt: ValueError für einen falschen Wert, TypeError für einen falschen Typ. Eine eigene, wenn der Aufrufer sie von den anderen unterscheiden und auf seine eigene Weise reagieren muss, wenn die Exception Daten tragen muss, wie shortfall, oder wenn es keine fertige mit dieser Bedeutung gibt, etwa für das Abheben von einem geschlossenen Konto.

Probier es aus

Fülle die Lücke

Eine Exception für ein ungültiges Alter. Was gehört in die Lücke, damit die Meldung bei der Basisklasse ankommt?

class InvalidAgeError(Exception):
    def __init__(self, message):
        ().__init__(message)

Übung

Exceptions in Klassen

Löse die Aufgaben in deinem Editor.

1. Musikgeschäft. Das Geschäft führt Instrumente vor, aber nicht jedes lässt sich vorführen.

  • Eine Klasse MusicalInstrument mit Name, Marke und Preis und __init__.
  • Eine abstrakte Klasse Playable(ABC) mit einer abstrakten Methode play() (@abstractmethod). So baut man in Python ein Interface.
  • Guitar und Piano erben sowohl von MusicalInstrument als auch von Playable und können spielen. Drum erbt nur vom Instrument.
  • class NotPlayableError(Exception).
  • MusicShop mit einer Methode demo_instrument(self, instrument): Wenn das Instrument spielen kann (isinstance(instrument, Playable)), führt sie es vor, sonst wirft sie NotPlayableError.
  • Führe im Programm eine Gitarre Fender Stratocaster (1299 €), ein Schlagzeug Pearl Export (849 €) und ein Klavier Yamaha P-45 (499 €) aus einem Array vor. Fange die Exception für jedes Instrument einzeln, damit die Vorführung weitergeht.
Ausgabe
Fender Stratocaster, 1299.00 €
Die Gitarre spielt einen Akkord.
Pearl Export, 849.00 €
Pearl Export kann nicht vorgeführt werden.
Yamaha P-45, 499.00 €
Das Klavier spielt eine Tonleiter.

2. Ein Konto mit Regeln. Füge Account eine Methode deposit(self, amount) hinzu, die bei einem Betrag von 0 oder weniger ValueError wirft. Das Abheben wirft InsufficientFundsError aus der Lektion. Das Konto startet mit 100 €. Lies die Anzahl der Operationen und dann die Operationen selbst, jede in einer eigenen Zeile: „einzahlen 50“ oder „abheben 500“. Gib nach jeder Operation den Kontostand aus oder warum sie abgelehnt wurde.

Eingabe
4
einzahlen 50
abheben 500
abheben -5
abheben 120
Ausgabe
Kontostand: 150 €
Es fehlen 350 €. Kontostand: 150 €
Der Betrag muss positiv sein. Kontostand: 150 €
Kontostand: 30 €

3. Ungültiges Alter. Person hat einen Namen und ein Alter. Die Property age (@property mit Setter) wirft InvalidAgeError aus der vorigen Karte, wenn das Alter unter 0 oder über 150 liegt. Auch der Konstruktor geht über self.age = age, damit niemand eine Person mit ungültigem Alter erzeugen kann. Erzeuge Maria (19), Peter (−3) und Ivan (200).

Ausgabe
Maria, 19 Jahre
Peter: Ungültiges Alter: -3
Ivan: Ungültiges Alter: 200

Weitere Aufgaben

  • Tischreservierung. Eine Klasse Restaurant mit einer Anzahl freier Tische und einer Methode zum Reservieren. Wenn keine Tische mehr frei sind, wirf deine eigene NoTablesError. Wenn die Zahl der Gäste 0 oder kleiner ist, wirf die fertige Exception für ein falsches Argument.
  • Ein Produkt mit Lagerbestand. Eine Klasse Product mit einem Lagerbestand und einer Methode sell(self, qty). Wenn der Bestand nicht reicht, wirf OutOfStockError, die mitträgt, wie viele Stück da sind. Der Shop fängt die Exception und bietet an, die vorhandenen zu verkaufen.
  • Musikgeschäft mit Grund. Füge NotPlayableError ein Feld mit dem Instrument hinzu, das nicht spielen kann, und gib im except seinen Preis aus.

Test

Teste dich

1. Was macht raise ValueError("...")?
2. Das Programm ruft a auf, a ruft b auf, b wirft eine Exception. Nur der Code auf oberster Ebene hat ein except. Was passiert mit den Zeilen in a nach dem Aufruf von b?
3. Wo fängst du die Exception am besten?
4. Von welcher Klasse erbt deine eigene Exception in Python?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·