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 -= amountDer Geldautomat benutzt sie so:
account = Account(100)account.withdraw(500)print("Nimm dein Geld.")print(f"Kontostand: {account.balance} €")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 -= amountraise 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} €")Abgelehnt: Nicht genug GeldKontostand: 100 €ValueError("Nicht genug Geld")erzeugt das Exception-Objekt wie jedes Objekt in Python. Die Meldung kommt in seinen Konstruktor, undprint(e)und der f-Stringf"{e}"geben sie zurück.ValueErrorist 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. raisefunktioniert nur mit einem Objekt einer Unterklasse vonBaseException. Einen String kannst du nicht werfen:raise "Nicht genug Geld"bricht mitTypeError: exceptions must derive from BaseExceptionab.
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
- Das Programm ruft pay_rent für ein Konto mit 500 € auf.
- pay_rent ruft withdraw(800) auf: ein neuer Frame oben drauf.
- 800 > 500: Die Methode wirft mit raise eine Exception.
- Der Kontostand ändert sich nicht: Der Frame wird verworfen, ohne seine Zeilen zu Ende auszuführen.
- pay_rent hat kein except: „Die Miete ist bezahlt.“ wird nicht ausgegeben, und der Frame wird verworfen.
- 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))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 GeldIn 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} €")„Ich bezahle die Bestellung“ kommt vor dem Abheben. Dann wirft withdraw, und der Frame von checkout wird verworfen: „Die Bestellung ist bezahlt“ erscheint nie. Im Programm wird auch „Ich sende die Bestätigung“ übersprungen, weil es im try nach dem Aufruf steht. except gibt die Ablehnung aus, und der Kontostand bleibt unberührt.
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.
except fängt die Exception, und pass heißt „nichts“: Python erlaubt keinen leeren Block, deshalb schreibt man pass. Der Fehler verschwindet, genau wie beim Geldautomaten in der ersten Karte. Fangen sollst du dort, wo du etwas Sinnvolles tun kannst. Hier kann pay_rent das Geld nicht herbeischaffen, also sollte die Funktion nicht fangen. Entferne try/except: Die Exception erreicht dann das Programm, und dort erfährt die Person davon.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
raiseohne etwas danach, imexcept. 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}")Log: Die Miete ist nicht durchgegangenAbgelehnt: 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 ine.__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__}")Die Miete ist nicht bezahlt, weil: Nicht genug GeldDie 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 = shortfallDeine 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 -= amountUnd 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}")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ängtexcept ValueErrorauch sie. Hier ist sie getrennt, deshalb spielt die Reihenfolge der beidenexcept-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 -= amountDas 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)super().__init__(message) ruft den Konstruktor der Basisklasse auf. Die Basisklasse speichert die Meldung, und str(e) gibt sie zurück. Eine Exception, die nur eine Meldung hat, kommt ohne __init__ aus: class InvalidAgeError(Exception): pass erbt den Konstruktor von Exception. __init__ brauchst du, wenn die Exception auch eigene Daten trägt, wie shortfall.
Ü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
MusicalInstrumentmit Name, Marke und Preis und__init__. - Eine abstrakte Klasse
Playable(ABC)mit einer abstrakten Methodeplay()(@abstractmethod). So baut man in Python ein Interface. GuitarundPianoerben sowohl vonMusicalInstrumentals auch vonPlayableund können spielen.Drumerbt nur vom Instrument.class NotPlayableError(Exception).MusicShopmit einer Methodedemo_instrument(self, instrument): Wenn das Instrument spielen kann (isinstance(instrument, Playable)), führt sie es vor, sonst wirft sieNotPlayableError.
- 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.
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.
4einzahlen 50abheben 500abheben -5abheben 120Kontostand: 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).
Maria, 19 JahrePeter: Ungültiges Alter: -3Ivan: Ungültiges Alter: 200Weitere Aufgaben
- Tischreservierung. Eine Klasse
Restaurantmit einer Anzahl freier Tische und einer Methode zum Reservieren. Wenn keine Tische mehr frei sind, wirf deine eigeneNoTablesError. 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
Productmit einem Lagerbestand und einer Methodesell(self, qty). Wenn der Bestand nicht reicht, wirfOutOfStockError, 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
NotPlayableErrorein Feld mit dem Instrument hinzu, das nicht spielen kann, und gib imexceptseinen Preis aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces als abstrakte Klassen · Typprüfungen · Exceptions · try, except und finally
- Geht weiter in: Daten in Dateien · dataclass, match und Muster · Entwurfsmuster · Tests mit unittest
- Aufgaben zum Thema: Fehler und Exceptions