Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion 2: Konstanten als Klassenvariablen. Was gibt das aus?
class Status:
OPEN = "open"
CLOSED = "closed"
status = "opne"
print(status == Status.OPEN, status in (Status.OPEN, Status.CLOSED))// I2.6 · ca. 40 Min. · Aufbau
In diesem Projekt definieren Sie Enums mit auto und StrEnum, finden Mitglieder über Wert und Name und bauen aus dem Modul ein Bankkonto-Modell mit Buchungen, einem Status und klaren Fehlern.
Lektion 6 von 6 in I2 Klassen
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
class Status:
OPEN = "open"
CLOSED = "closed"
status = "opne"
print(status == Status.OPEN, status in (Status.OPEN, Status.CLOSED))Vorhersagen · Aufgabe 2 von 7
from enum import Enum
class Status(Enum):
OPEN = "open"
CLOSED = "closed"
print(Status("open"))
try:
Status("opne")
except ValueError as err:
print(err)Üben · Aufgabe 3 von 7
from enum import Enum
class Status(Enum):
OPEN = "open"
CLOSED = "closed"
print(Status.OPEN.____)Üben · Aufgabe 4 von 7
from enum import Enum, auto
class Level(Enum):
LOW = auto()
MID = auto()
HIGH = auto()
print([m.value for m in Level])Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
from enum import Enum
class Size(Enum):
S = 1
SMALL = 1
M = 2
print(len(Size), Size.SMALL)Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Status ist ein Enum mit Zahlen aus auto(), und ein Dict ordnet jedem Status den nächsten zu: Mitglieder sind hashbar und taugen als Schlüssel. Channel ist ein StrEnum, seine Mitglieder sind also gleich einfachen Strings und zeigen in f-Strings ihren Wert. Text von außen wie "fax" wird an einer Stelle in ein Mitglied umgewandelt, wo sich ein ValueError fangen lässt.
main.py
from enum import Enum, StrEnum, auto
class Status(Enum):
PENDING = auto()
PAID = auto()
SHIPPED = auto()
class Channel(StrEnum):
WEB = auto()
PHONE = auto()
NEXT = {Status.PENDING: Status.PAID, Status.PAID: Status.SHIPPED}
def advance(status: Status) -> Status:
if status not in NEXT:
raise ValueError(f"{status.name} is the last step")
return NEXT[status]
print([status.name for status in Status])
print(Status(2), Status["SHIPPED"].value, Status.PAID is Status(2))
print(advance(Status.PENDING), repr(advance(Status.PAID)))
print(Channel.WEB == "web", f"via {Channel.PHONE}", Channel("phone").name)
for text in ["web", "fax"]:
try:
print("channel:", Channel(text))
except ValueError as err:
print("refused:", err)
try:
advance(Status.SHIPPED)
except ValueError as err:
print("refused:", err)
Ausführen mit
python main.pyAusgabe
['PENDING', 'PAID', 'SHIPPED']
Status.PAID 3 True
Status.PAID <Status.SHIPPED: 3>
True via phone PHONE
channel: web
refused: 'fax' is not a valid Channel
refused: SHIPPED is the last stepTab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übung 1 von 3
Beginnen Sie das Bankmodell mit Beträgen in Cent. Machen Sie TxKind zu einem StrEnum mit Werten aus auto(), damit TxKind("withdrawal") funktioniert. Machen Sie Transaction zu einer eingefrorenen Datenklasse. Ihr __post_init__ löst ValueError aus, wenn cents 0 oder kleiner ist. Ergänzen Sie eine Property signed: cents bei einer Einzahlung, minus cents bei einer Abhebung.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Ersetzen Sie die String-Werte durch auto(): In einem StrEnum liefert DEPOSIT = auto() den Wert "deposit".
Schreiben Sie @dataclass(frozen=True) und lösen Sie in __post_init__(self) -> None ValueError aus, wenn self.cents <= 0.
Die Property vergleicht die Art mit is: Bei self.kind is TxKind.DEPOSIT geben Sie self.cents zurück, sonst -self.cents.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from dataclasses import dataclass
from enum import StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = auto()
WITHDRAWAL = auto()
@dataclass(frozen=True)
class Transaction:
kind: TxKind
cents: int
def __post_init__(self) -> None:
if self.cents <= 0:
raise ValueError(f"amount must be positive, got {self.cents}")
@property
def signed(self) -> int:
if self.kind is TxKind.DEPOSIT:
return self.cents
return -self.cents
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
from dataclasses import dataclass
from enum import StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = "DEPOSIT"
WITHDRAWAL = "WITHDRAWAL"
@dataclass
class Transaction:
kind: TxKind
cents: int
test_main.py
import dataclasses
from main import Transaction, TxKind
def test_kind_values():
"""auto() liefert die kleingeschriebenen Namen als Werte"""
got = [kind.value for kind in TxKind]
assert got == ["deposit", "withdrawal"], f"die Werte sind {got!r}"
def test_lookup():
"""TxKind("withdrawal") findet das Mitglied"""
assert TxKind("withdrawal") is TxKind.WITHDRAWAL, 'TxKind("withdrawal") lieferte nicht TxKind.WITHDRAWAL'
def test_signed():
"""Einzahlungen zählen positiv, Abhebungen negativ"""
got = (Transaction(TxKind.DEPOSIT, 500).signed, Transaction(TxKind.WITHDRAWAL, 200).signed)
assert got == (500, -200), f"signed liefert {got!r}, erwartet ist (500, -200)"
def test_rejects_amount():
"""Ein Betrag von 0 löst ValueError aus"""
try:
Transaction(TxKind.DEPOSIT, 0)
except ValueError:
pass
else:
assert False, "Transaction(TxKind.DEPOSIT, 0) wurde angenommen"
def test_frozen():
"""Eine Buchung lässt sich nicht ändern"""
tx = Transaction(TxKind.DEPOSIT, 500)
try:
tx.cents = 1 # type: ignore[misc]
except dataclasses.FrozenInstanceError:
pass
else:
assert False, "tx.cents = 1 wurde angenommen; verwenden Sie frozen=True"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 3
Ihr Code aus Schritt 1 und zwei Exception-Klassen stehen bereit. Schreiben Sie Account um: Statt eines gespeicherten Kontostands führt es history, eine Liste von Transaction, die für jedes Konto neu ist. balance wird eine nur lesbare Property, die die Beträge mit Vorzeichen addiert. deposit und withdraw hängen eine Transaction an. withdraw löst InsufficientFundsError(balance, cents) aus, wenn der Betrag größer als der Kontostand ist.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Das Feld lautet history: list[Transaction] = field(default_factory=list). Entfernen Sie das Feld balance.
Die Property lautet @property def balance(self) -> int: return sum(tx.signed for tx in self.history).
Prüfen Sie in withdraw cents > self.balance vor dem Anhängen, damit eine abgelehnte Abhebung keine Spur hinterlässt. Transaction selbst lehnt Beträge von 0 ab.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from dataclasses import dataclass, field
from enum import StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = auto()
WITHDRAWAL = auto()
@dataclass(frozen=True)
class Transaction:
kind: TxKind
cents: int
def __post_init__(self) -> None:
if self.cents <= 0:
raise ValueError(f"amount must be positive, got {self.cents}")
@property
def signed(self) -> int:
if self.kind is TxKind.DEPOSIT:
return self.cents
return -self.cents
class AccountError(Exception):
"""Something the account cannot do."""
class InsufficientFundsError(AccountError):
def __init__(self, balance: int, requested: int) -> None:
super().__init__(f"balance {balance} is less than {requested}")
self.balance = balance
self.requested = requested
@dataclass
class Account:
owner: str
history: list[Transaction] = field(default_factory=list)
@property
def balance(self) -> int:
return sum(tx.signed for tx in self.history)
def deposit(self, cents: int) -> None:
self.history.append(Transaction(TxKind.DEPOSIT, cents))
def withdraw(self, cents: int) -> None:
if cents > self.balance:
raise InsufficientFundsError(self.balance, cents)
self.history.append(Transaction(TxKind.WITHDRAWAL, cents))
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
from dataclasses import dataclass, field
from enum import StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = auto()
WITHDRAWAL = auto()
@dataclass(frozen=True)
class Transaction:
kind: TxKind
cents: int
def __post_init__(self) -> None:
if self.cents <= 0:
raise ValueError(f"amount must be positive, got {self.cents}")
@property
def signed(self) -> int:
if self.kind is TxKind.DEPOSIT:
return self.cents
return -self.cents
class AccountError(Exception):
"""Something the account cannot do."""
class InsufficientFundsError(AccountError):
def __init__(self, balance: int, requested: int) -> None:
super().__init__(f"balance {balance} is less than {requested}")
self.balance = balance
self.requested = requested
@dataclass
class Account:
owner: str
balance: int = 0
def deposit(self, cents: int) -> None:
self.balance += cents
def withdraw(self, cents: int) -> None:
self.balance -= cents
test_main.py
from main import Account, InsufficientFundsError, TxKind
def test_starts_empty():
"""Ein neues Konto hat einen leeren Verlauf und Kontostand 0"""
acct = Account("ada")
assert (getattr(acct, "history", None), acct.balance) == ([], 0), f"history {getattr(acct, 'history', None)!r}, balance {acct.balance!r}"
def test_balance_from_history():
"""Der Kontostand wird aus dem Verlauf berechnet"""
acct = Account("ada")
acct.deposit(1000)
acct.withdraw(300)
kinds = [tx.kind for tx in getattr(acct, "history", [])]
assert kinds == [TxKind.DEPOSIT, TxKind.WITHDRAWAL], f"der Verlauf enthält {kinds!r}"
assert acct.balance == 700, f"balance ist {acct.balance!r}, erwartet ist 700"
def test_own_history():
"""Jedes Konto hat seinen eigenen Verlauf"""
a = Account("ada")
b = Account("bob")
a.deposit(500)
got = getattr(b, "history", None)
assert got == [], f"nach einer Einzahlung auf a ist b.history {got!r}"
def test_insufficient_funds():
"""Zu viel abheben löst InsufficientFundsError aus"""
acct = Account("ada")
acct.deposit(100)
try:
acct.withdraw(250)
except InsufficientFundsError as err:
assert (err.balance, err.requested) == (100, 250), f"der Fehler enthält {(err.balance, err.requested)!r}"
else:
assert False, "withdraw(250) bei Kontostand 100 wurde angenommen"
assert acct.balance == 100, f"nach der abgelehnten Abhebung ist balance {acct.balance!r}"
def test_rejects_zero():
"""deposit(0) löst ValueError aus"""
try:
Account("ada").deposit(0)
except ValueError:
pass
else:
assert False, "deposit(0) wurde angenommen"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 3 von 3
Geben Sie Status die Mitglieder OPEN und FROZEN über auto() und Account ein Feld status, das mit Status.OPEN beginnt. deposit und withdraw lösen AccountError("account is frozen") aus, wenn das Konto nicht offen ist. apply(rows) bucht Zeilen wie ("deposit", 500): Schlagen Sie die Art mit TxKind(text) nach. Eine Zeile, die mit ValueError oder AccountError scheitert, wird übersprungen und als "row 2: " plus Meldung gemeldet. Geben Sie die Meldungen zurück.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Ergänzen Sie status: Status = Status.OPEN nach history. Ein Mitglied ist ein zulässiger Standardwert, denn es ist keine Liste.
Eine Hilfsmethode _check_open löst AccountError(f"account is {self.status.name.lower()}") aus, wenn self.status is not Status.OPEN; rufen Sie sie in deposit und withdraw zuerst auf.
Iterieren Sie mit for number, (kind_text, cents) in enumerate(rows, start=1) und fangen Sie (ValueError, AccountError) as err um Nachschlagen und Buchen herum.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from dataclasses import dataclass, field
from enum import Enum, StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = auto()
WITHDRAWAL = auto()
@dataclass(frozen=True)
class Transaction:
kind: TxKind
cents: int
def __post_init__(self) -> None:
if self.cents <= 0:
raise ValueError(f"amount must be positive, got {self.cents}")
@property
def signed(self) -> int:
if self.kind is TxKind.DEPOSIT:
return self.cents
return -self.cents
class AccountError(Exception):
"""Something the account cannot do."""
class InsufficientFundsError(AccountError):
def __init__(self, balance: int, requested: int) -> None:
super().__init__(f"balance {balance} is less than {requested}")
self.balance = balance
self.requested = requested
class Status(Enum):
OPEN = auto()
FROZEN = auto()
@dataclass
class Account:
owner: str
history: list[Transaction] = field(default_factory=list)
status: Status = Status.OPEN
@property
def balance(self) -> int:
return sum(tx.signed for tx in self.history)
def _check_open(self) -> None:
if self.status is not Status.OPEN:
raise AccountError(f"account is {self.status.name.lower()}")
def deposit(self, cents: int) -> None:
self._check_open()
self.history.append(Transaction(TxKind.DEPOSIT, cents))
def withdraw(self, cents: int) -> None:
self._check_open()
if cents > self.balance:
raise InsufficientFundsError(self.balance, cents)
self.history.append(Transaction(TxKind.WITHDRAWAL, cents))
def apply(self, rows: list[tuple[str, int]]) -> list[str]:
problems: list[str] = []
for number, (kind_text, cents) in enumerate(rows, start=1):
try:
kind = TxKind(kind_text)
if kind is TxKind.DEPOSIT:
self.deposit(cents)
else:
self.withdraw(cents)
except (ValueError, AccountError) as err:
problems.append(f"row {number}: {err}")
return problems
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
from dataclasses import dataclass, field
from enum import Enum, StrEnum, auto
class TxKind(StrEnum):
DEPOSIT = auto()
WITHDRAWAL = auto()
@dataclass(frozen=True)
class Transaction:
kind: TxKind
cents: int
def __post_init__(self) -> None:
if self.cents <= 0:
raise ValueError(f"amount must be positive, got {self.cents}")
@property
def signed(self) -> int:
if self.kind is TxKind.DEPOSIT:
return self.cents
return -self.cents
class AccountError(Exception):
"""Something the account cannot do."""
class InsufficientFundsError(AccountError):
def __init__(self, balance: int, requested: int) -> None:
super().__init__(f"balance {balance} is less than {requested}")
self.balance = balance
self.requested = requested
class Status(Enum):
"""Add the members OPEN and FROZEN."""
@dataclass
class Account:
owner: str
history: list[Transaction] = field(default_factory=list)
@property
def balance(self) -> int:
return sum(tx.signed for tx in self.history)
def deposit(self, cents: int) -> None:
self.history.append(Transaction(TxKind.DEPOSIT, cents))
def withdraw(self, cents: int) -> None:
if cents > self.balance:
raise InsufficientFundsError(self.balance, cents)
self.history.append(Transaction(TxKind.WITHDRAWAL, cents))
def apply(self, rows: list[tuple[str, int]]) -> list[str]:
return []
test_main.py
from main import Account, AccountError, Status
def test_status_members():
"""Status hat OPEN und FROZEN, in dieser Reihenfolge"""
got = [status.name for status in Status]
assert got == ["OPEN", "FROZEN"], f"die Mitglieder sind {got!r}"
def test_starts_open():
"""Ein neues Konto ist offen"""
got = getattr(Account("ada"), "status", None)
assert got is Status.OPEN, f"der Status eines neuen Kontos ist {got!r}"
def test_frozen_refuses():
"""Ein eingefrorenes Konto lehnt Einzahlungen mit AccountError ab"""
acct = Account("ada", status=Status.FROZEN)
try:
acct.deposit(500)
except AccountError as err:
assert str(err) == "account is frozen", f"die Meldung ist {str(err)!r}"
else:
assert False, "ein eingefrorenes Konto hat eine Einzahlung angenommen"
def test_apply_good_rows():
"""apply bucht gültige Zeilen und meldet keine Probleme"""
acct = Account("ada")
problems = acct.apply([("deposit", 1000), ("withdrawal", 400)])
assert (problems, acct.balance) == ([], 600), f"Probleme {problems!r}, balance {acct.balance!r}"
def test_apply_collects_problems():
"""apply überspringt fehlerhafte Zeilen und meldet sie mit Nummer"""
acct = Account("ada")
rows = [("deposit", 500), ("refund", 100), ("withdrawal", 900), ("deposit", 0), ("withdrawal", 200)]
problems = acct.apply(rows)
expected = [
"row 2: 'refund' is not a valid TxKind",
"row 3: balance 500 is less than 900",
"row 4: amount must be positive, got 0",
]
assert problems == expected, f"apply lieferte {problems!r}"
assert acct.balance == 300, f"balance ist {acct.balance!r}, erwartet ist 300"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenfrom enum import Enum
class Status(Enum):
OPEN = "open"
CLOSED = "closed"
status = Status("opne")
Was Python ausgibt
ValueError: 'opne' is not a valid StatusWarum, und die Lösung
Dieser Fehler ist genau die Aufgabe des Enums: Der Text ist keiner der erlaubten Werte. Kommt der Text von Nutzern oder aus einer Datei, wandeln Sie ihn an einer Stelle um und fangen dort ValueError, um erneut zu fragen oder die Zeile zu melden. Greifen Sie nicht auf den rohen String zurück, sonst wandert der Tippfehler weiter.
from enum import Enum
class Level(Enum):
LOW = 1
HIGH = 2
print(Level.LOW < Level.HIGH)
Was Python ausgibt
TypeError: '<' not supported between instances of 'Level' and 'Level'Warum, und die Lösung
Enum-Mitglieder unterstützen == und is, aber nicht <, auch wenn ihre Werte Zahlen sind. Vergleichen Sie stattdessen die Werte, Level.LOW.value < Level.HIGH.value, oder halten Sie eine ausdrückliche Reihenfolge fest, etwa list(Level).index(member).
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
print(Color["red"])
Was Python ausgibt
KeyError: 'red'Warum, und die Lösung
Eckige Klammern schlagen den Mitgliedsnamen genau so nach, wie er in der Klasse steht, hier RED. Normalisieren Sie den Text zuerst, Color[text.strip().upper()], und fangen Sie KeyError für Namen, die es trotzdem nicht gibt. Ist der Text ein Wert und kein Name, nehmen Sie runde Klammern.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.