Abschlussprojekt: Eine gemeinschaftliche Leihbibliothek · Übung
Zustand absichern und Fehler entwerfen
Die Bibliothek lehnt die richtigen Dinge ab. Sie formuliert ihre Ablehnungen nur schlecht.
Versuche, einen bereits ausgeliehenen Gegenstand auszuleihen, und lies, was zurückkommt:
ItemUnavailableError: ('b1', 24)
Das ist Pythons Standard: Ohne eigenen Konstruktor speichert eine Ausnahme alles Übergebene und gibt das Tupel aus. Die Information ist vollständig vorhanden, wird aber als Gepäckstück statt als Satz präsentiert.
Eine Ausnahme ist ein von dir entworfenes Objekt
Kapitel 12 hat dieses Argument gemacht, und hier zahlt es sich aus. Eine Ausnahme ist kein Etikett. Sie ist ein Objekt, erreicht einen Aufrufer, der etwas damit tun muss, und ihr Inhalt ist eine Entwurfsentscheidung.
Zwei Zielgruppen brauchen unterschiedliche Dinge aus demselben Fehler.
Eine Person braucht einen Satz: item ‘b1’ is on loan until day 24. Sie sollte nicht wissen müssen, in welcher Reihenfolge die Argumente übergeben wurden.
Ein Programm braucht die Einzelteile: Die Oberfläche, die „Versuche es nach Tag 24 erneut“ sagt, braucht 24 als Zahl und nicht als Teilstring, der wieder aus einem englischen Satz herausgelesen werden muss.
Eine gut gestaltete Ausnahme bedient beide. Dafür nimmt ein Konstruktor die Teile entgegen, speichert sie als Attribute und baut den Satz selbst:
class ItemUnavailableError(LibraryError):
def __init__(self, item_id, due_day):
super().__init__(f"item {item_id!r} is on loan until day {due_day}")
self.item_id = item_id
self.due_day = due_day
super().__init__(...) macht str(error) zu diesem Satz. Die beiden Zuweisungen machen die Einzelteile zugänglich. Keines ersetzt das andere.
Warum die Aufrufstellen unverändert bleiben
Schau dir an, was checkout bereits schreibt:
raise ItemUnavailableError(item_id, self._loans[item_id].due_day)
Es übergibt die Einzelteile schon die ganze Zeit. Es hat nie eine Meldung gebaut und hätte das auch nie tun sollen: Eine Methode tief in der Bibliothek weiß nicht, ob ihr Aufrufer eine Webseite, ein Test oder ein Terminal ist. Deshalb ist der Wortlaut nicht ihre Aufgabe.
Darum bearbeitet diese Lektion nur errors.py. Die Information floss bereits. Du gibst ihr einen festen Platz.
Warum due_day als Attribut speichern, wenn es bereits in der Meldung erscheint?
Die Familie und die Aufgabe der Basisklasse
LibraryError bleibt exakt unverändert: eine einfache Unterklasse von Exception mit einem Docstring und sonst nichts.
Das ist keine Bequemlichkeit. Ihre ganze Aufgabe besteht darin, ein abfangbarer Name zu sein:
try:
library.checkout(item_id, member_id, today)
except LibraryError as error:
print(f"Sorry: {error}")
Eine Fehlerbehandlung, jede Ablehnung, ein Satz pro Fall. Ein Aufrufer, der spezifischer reagieren möchte, fängt zuerst ItemUnavailableError ab und liest error.due_day. Die Ersetzbarkeitsregel aus Kapitel 9 ermöglicht beides zugleich: Jede der vier Ausnahmen ist ein LibraryError. Wer die Basisklasse abfängt, kann deshalb keine davon verpassen.
Eine fünfte Art von Ablehnung später hinzuzufügen bedeutet, eine Klasse hinzuzufügen. Es bedeutet nicht, alle except-Klauseln erneut zu bearbeiten.
Was du jetzt baust
Gib jeder der vier Ausnahmen einen Konstruktor, der die bereits von library.py übergebenen Teile entgegennimmt, daraus eine lesbare Meldung baut und die Teile als Attribute festhält.
Lass LibraryError unverändert.
Aufgabe
Gib den vier Domänenausnahmen in errors.py echte Konstruktoren.
Jeder nimmt die bereits von library.py übergebenen Teile entgegen, hält sie als gleichnamige Attribute fest und erstellt mit super().__init__(...) seine eigene Meldung:
UnknownItemError(item_id):"no item with id 'b1'", Attributitem_id.UnknownMemberError(member_id):"no member with id 'm1'", Attributmember_id.ItemUnavailableError(item_id, due_day):"item 'b1' is on loan until day 24", Attributeitem_idunddue_day.LoanLimitReachedError(member_id, limit):"member 'm1' already holds 2 item(s)", Attributemember_idundlimit.
Verwende !r für die IDs, damit die Anführungszeichen deutlich zeigen, wo eine ID beginnt und endet.
Lass LibraryError exakt unverändert. Seine Aufgabe ist, der eine Name zu sein, der alle vier abfängt.