dataclass, match und Muster
Kurz gesagt: @dataclass macht eine Datenklasse, für die Python __init__, __eq__ und __repr__ selbst schreibt, und match wählt einen Fall nach dem Typ und den Feldern des Objekts.
Problem
Der Platz ist belegt, aber das Dictionary findet ihn nicht
Reservierungen im Kino: Der Schlüssel ist der Platz (Reihe und Nummer), der Wert ist der Name der Person.
class Seat: def __init__(self, row, number): self.row = row self.number = numberreservations = {}reservations[Seat(5, 12)] = "Maria"
print(reservations.get(Seat(5, 12)))print(Seat(5, 12) in reservations)NoneFalseget gibt None zurück. Warum, weißt du noch aus der Lektion über Hashcodes: Seat(5, 12) zweimal geschrieben sind zwei verschiedene Objekte, und Seat sagt nicht, wann zwei davon gleich sind. Die Lösung sind __eq__ und __hash__, dazu __repr__ für die Ausgabe. Noch etwa zehn Zeilen für eine Klasse, die nur zwei Zahlen trägt.Die Idee
@dataclass: eine Klasse für Daten
@dataclass braucht dasselbe ein paar Zeilen:from dataclasses import dataclass
@dataclass(frozen=True)class Seat: row: int number: intreservations = {}reservations[Seat(5, 12)] = "Maria"
print(reservations.get(Seat(5, 12)))print(Seat(5, 12) in reservations)
seat = Seat(5, 12)print(seat)print(f"Reihe {seat.row}, Platz {seat.number}")MariaTrueSeat(row=5, number=12)Reihe 5, Platz 12@dataclass (aus dem Modul dataclasses) vor der Klasse sagt, dass ihre Aufgabe ist, Daten zu tragen. Die Felder stehen im Rumpf mit ihrem Typ: row: int. Daraus schreibt Python für dich:
- einen Konstruktor
__init__mit allen Feldern; __eq__, das die Felder vergleicht;- mit
frozen=True: ein Verbot, die Felder nach dem Erzeugen zu ändern, und ein__hash__, der aus den Feldern berechnet wird. Deshalb findetgetMaria jetzt; __repr__in der FormSeat(row=5, number=12).
Die Felder liest du direkt: seat.row. Ein Objekt mit frozen=True ändert sich nach dem Erzeugen nicht.
reservations = {}reservations[Seat(5, 12)] = "Maria"
print(reservations.get(Seat(5, 12)))print(Seat(5, 12) in reservations)
seat = Seat(5, 12)print(seat)print(f"Reihe {seat.row}, Platz {seat.number}")Schritt für Schritt, in Worten
- Ein leeres Dictionary: Der Schlüssel ist ein Kinoplatz, der Wert ist ein Name.
- Die Zuweisung legt den Schlüssel Seat(5, 12) mit dem Wert „Maria“ an.
- get bekommt ein neues Seat-Objekt. Das ist ein anderes Objekt im Speicher, nicht der Schlüssel aus der Zuweisung.
- Der __hash__ einer Dataclass wird aus den Feldern berechnet, deshalb ist er gleich: Die Suche geht an dieselbe Stelle.
- __eq__ einer Dataclass vergleicht Feld für Feld: Reihe 5 und 5, Platz 12 und 12. Sie sind gleich, und get gibt „Maria“ zurück.
- in macht dasselbe mit einem weiteren neuen Seat-Objekt: True.
- Auch __repr__ schreibt die Dataclass selbst, und die Felder liest man direkt: seat.row.
Genauer, für Neugierige
- Ohne
frozen=Truekönnen sich die Felder ändern, und__hash__wird nicht geschrieben. Dann kann das Objekt kein Schlüssel in einem Dictionary sein:
from dataclasses import dataclass
@dataclassclass Seat: row: int number: int
seat = Seat(5, 12)seat.row = 6print(seat)try: reservations = {seat: "Maria"}except TypeError as e: print("TypeError:", e)Seat(row=6, number=12)TypeError: unhashable type: 'Seat'- Der Versuch, ein Feld eines
frozen-Objekts zu ändern, bricht mit einemFrozenInstanceErrorab. Und der Typ des Feldes ist nur ein Hinweis: Python prüft ihn nicht, undSeat("fünfte", 12)geht durch:
from dataclasses import dataclass
@dataclass(frozen=True)class Seat: row: int number: int
seat = Seat(5, 12)try: seat.row = 6except AttributeError as e: print(type(e).__name__ + ":", e)print(Seat("fünfte", 12))FrozenInstanceError: cannot assign to field 'row'Seat(row='fünfte', number=12)- Die Unveränderlichkeit reicht nur eine Ebene tief. Wenn das Feld eine Liste ist, zeigt es immer auf dieselbe Liste, aber die Liste selbst kann sich ändern:
from dataclasses import dataclass
@dataclass(frozen=True)class Cart: items: list
cart = Cart(["Kaffee"])cart.items.append("Honig")print(cart)Cart(items=['Kaffee', 'Honig'])- In der Lektion über Hashcodes hast du
__eq__und__hash__von Hand geschrieben. Mit@dataclasshast du sie schon.
Die Idee
Prüfungen und Methoden in einer Dataclass
__post_init__: @dataclass ruft sie direkt auf, nachdem es die Felder gesetzt hat.from dataclasses import dataclass
@dataclass(frozen=True)class Price: amount: float currency: str
def __post_init__(self): if self.amount < 0: raise ValueError(f"Der Preis darf nicht negativ sein: {self.amount}")
def with_discount(self, percent): return Price(self.amount * (1 - percent / 100), self.currency)coffee = Price(18.50, "EUR")sale = coffee.with_discount(20)print(coffee)print(sale)
try: Price(-5, "EUR")except ValueError as e: print(e)Price(amount=18.5, currency='EUR')Price(amount=14.8, currency='EUR')Der Preis darf nicht negativ sein: -5- In
__post_init__sind die Felder schon gesetzt, deshalb liest die Prüfungself.amount. Wenn sie eine Exception auslöst, wird das Objekt nicht erzeugt. with_discountändert den Preis nicht. Es gibt einen neuenPricezurück. So arbeitest du mit Objekten, die sich nicht ändern:coffeebleibt bei 18,50 €.
Probier es aus
Sind sie gleich?
Seat ist eine @dataclass(frozen=True), wie in der Karte davor.Was gibt das Programm aus?
a = Seat(5, 12)b = Seat(5, 12)print(a is b)print(a == b)print(hash(a) == hash(b))a und b sind zwei Objekte, deshalb ist is False: Es fragt, ob es dasselbe Objekt ist. == ruft das __eq__ der Dataclass auf, das die Felder vergleicht, und die sind gleich. Auch hash wird aus den Feldern berechnet, also ist es ebenfalls gleich. Genau das will das Dictionary.Probier es aus
Die Lieferung wird zu teuer
Die Lieferung in eine Filiale kostet 4,50 €, aber das Programm sagt 6,90 €. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
method == "kurier" or "adresse" wird als (method == "kurier") or "adresse" gelesen. Der erste Teil ist False, aber "adresse" ist ein nicht leerer String und zählt als wahr („truthy“). Die Bedingung ist für jede Lieferart wahr, und die Filiale kommt nie bis zu ihrem elif. Die Lösung ist method in ("kurier", "adresse"). Noch klarer ist ein match, bei dem jeder Fall seine Werte aufzählt. Das ist die nächste Karte.Die Idee
match: Fall für Fall
match. Es vergleicht den Wert der Reihe nach mit den Fällen, von oben nach unten, und führt den ersten aus, der passt.def delivery_price(method): match method: case "filiale": return 4.50 case "kurier" | "adresse": return 6.90 case "laden": return 0 case _: raise ValueError(f"Unbekannte Lieferart: {method}")
price = delivery_price("filiale")print(f"Lieferung: {price} €")Lieferung: 4.5 €- Es gibt kein Durchfallen: Nur der Code des Falls, der passt, läuft.
|bedeutet „oder“:case "kurier" | "adresse".case _passt auf alles. Er steht zuletzt und fängt den Rest, wiedefaultin Java. Hier löst er eine Exception aus.matchliefert von selbst keinen Wert. Deshalb steht es in einer Funktion, und jeder Fall kehrt mitreturnzurück.- Python prüft nicht, dass jede Eingabe abgedeckt ist. Ohne
case _passt kein Fall auf eine unbekannte Lieferart: Die Funktion kommt an ihr Ende und gibtNonezurück, ohne Fehler:
def delivery_price(method): match method: case "filiale": return 4.50 case "kurier": return 6.90
print(delivery_price("post"))NoneEin Fall mit mehreren Zeilen ist ein normaler eingerückter Block:
def delivery(method, weight_kg): match method: case "filiale": return 4.50 case "kurier": base = 6.90 return base + 3 if weight_kg > 10 else base case _: return 0
print(delivery("kurier", 4))print(delivery("kurier", 12))6.99.9Genauer, für Neugierige
match gibt es seit Python 3.10. In älterem Code ist dasselbe eine Kette aus if und elif.
Die Idee
Eine Familie von Typen
Der Shop nimmt drei Zahlungsarten an: Karte, Banküberweisung und Nachnahme. Andere gibt es nicht. Jede Art ist eine eigene Klasse:
from dataclasses import dataclass
@dataclass(frozen=True)class CardPayment: card_number: str
@dataclass(frozen=True)class BankTransfer: iban: str
@dataclass(frozen=True)class CashOnDelivery: pass
Payment = CardPayment | BankTransfer | CashOnDeliveryJede Zahlungsart ist nur Daten: eine Kartennummer, eine IBAN oder nichts. Deshalb sind sie @dataclass. CashOnDelivery hat keine Felder: Sein Rumpf ist pass.
Die letzte Zeile ist ein Typhinweis: Payment ist „eine der drei“. In Java sorgt sealed dafür, dass der Compiler einen vierten Typ ablehnt. Python hat kein solches Verbot: Der Hinweis beschreibt nur die Absicht, und Python prüft ihn nicht. Eine vierte Klasse kann überall auftauchen. Wie du dich davor schützt, steht in der nächsten Karte.
Sieh es dir an
Muster in match
match kann auch den Typ des Objekts prüfen:def fee(payment, amount): match payment: case CardPayment(): return amount * 0.015 case BankTransfer(): return 0.50 case CashOnDelivery(): return 2.00print(fee(CardPayment("4111222233334444"), 200))print(fee(BankTransfer("DE89370400440532013000"), 200))print(fee(CashOnDelivery(), 200))3.00.52.0case CardPayment(): bedeutet „wenn das Objekt vom Typ CardPayment ist“. Die Klammern erzeugen hier kein Objekt: Das ist ein Muster, kein Aufruf. Es ist dasselbe wie isinstance(payment, CardPayment), aber für viele Typen auf einmal.
Wenn du einen Typ vergisst, sagt Python nichts. Kein Fall passt, und die Funktion gibt None zurück:
def fee(payment, amount): match payment: case CardPayment(): return amount * 0.015 case BankTransfer(): return 0.50
print(fee(CashOnDelivery(), 200))NoneSetz deshalb ans Ende einen case _, der eine Exception auslöst. Python prüft trotzdem nicht im Voraus, aber wenigstens stoppt das Programm mit einer klaren Meldung an der Stelle des Ausrutschers:
def fee(payment, amount): match payment: case CardPayment(): return amount * 0.015 case BankTransfer(): return 0.50 case _: raise TypeError(f"Unbekannte Zahlung: {payment}")
try: fee(CashOnDelivery(), 200)except TypeError as e: print(e)Unbekannte Zahlung: CashOnDelivery()Ein Klassenmuster holt die Felder direkt heraus: CardPayment(number) legt die Kartennummer in number. Das funktioniert, weil @dataclass auch die Reihenfolge der Felder festhält. Das if nach dem Muster ist ein Guard: Der Fall gilt nur, wenn die Bedingung wahr ist.
def describe(payment): match payment: case CardPayment(number): return f"Karte ****{number[-4:]}" case BankTransfer(iban) if iban.startswith("DE"): return "Überweisung von einem deutschen Konto" case BankTransfer(iban): return "Überweisung aus dem Ausland" case CashOnDelivery(): return "Nachnahme"print(describe(CardPayment("4111222233334444")))print(describe(BankTransfer("DE89370400440532013000")))print(describe(BankTransfer("AT611904300234573201")))print(describe(CashOnDelivery()))Karte ****4444Überweisung von einem deutschen KontoÜberweisung aus dem AuslandNachnahmeGenauer, für Neugierige
- Eine einzelne Zahlung lässt sich auch mit
isinstanceprüfen. Und im Muster kannst du das Feld auch beim Namen nennen:CardPayment(card_number=number).
payment = CardPayment("4111222233334444")if isinstance(payment, CardPayment): print("Die Karte endet auf " + payment.card_number[12:])
match payment: case CardPayment(card_number=number): print("Und mit dem Feldnamen: " + number[-4:])Die Karte endet auf 4444Und mit dem Feldnamen: 4444- Muster lassen sich verschachteln:
case Order(Customer(name), total).
Probier es aus
Welcher Fall gewinnt?
Was gibt das Programm aus?
def label(payment, amount): match payment: case CashOnDelivery() if amount > 500: return "Nachnahme: über 500 € nicht möglich" case CashOnDelivery(): return "Nachnahme" case CardPayment() if amount > 1000: return "Karte: Bestätigung nötig" case CardPayment(): return "Karte" case BankTransfer(): return "Überweisung"
print(label(CashOnDelivery(), 650))print(label(CashOnDelivery(), 80))print(label(CardPayment("4111222233334444"), 650))if-Guard und landen beim zweiten. Die Kartenzahlung beträgt 650 €, und eine Bestätigung braucht es erst über 1000 €, also gewinnt der Fall ohne Bedingung: „Karte“.Genauer, für Neugierige
Der Fall mit Guard muss vor dem allgemeinen für denselben Typ stehen. Andersherum ist es kein Fehler: Der allgemeine Fall fängt alles, und der mit dem Guard wird einfach nie erreicht:
def label(payment, amount): match payment: case CashOnDelivery(): return "Nachnahme" case CashOnDelivery() if amount > 500: return "Nachnahme: über 500 € nicht möglich"
print(label(CashOnDelivery(), 650))NachnahmePython stoppt nur bei einem case _, der nicht der letzte ist. Er fängt alles, und der Code startet gar nicht erst:
code = """match payment: case _: print("alles") case CashOnDelivery(): print("Nachnahme")"""try: compile(code, "shop.py", "exec")except SyntaxError as e: print("SyntaxError:", e.msg)SyntaxError: wildcard makes remaining patterns unreachableDie Idee
Text über mehrere Zeilen
Text über mehrere Zeilen, wie ein Kassenbon oder JSON, ist mit " und \n in jeder Zeile schwer zu lesen. In Python kann der Text zwischen """ und """ über mehrere Zeilen gehen:
receipt = """\Der EckladenKaffeebohnen 18.50 €Honig 12.00 €Summe: 30.50 €"""print(receipt, end="")
name = "Maria"total = 75.5data = f'{{"customer": "{name}", "total": {total:.2f}}}'print(data)Der EckladenKaffeebohnen 18.50 €Honig 12.00 €Summe: 30.50 €{"customer": "Maria", "total": 75.50}- Ein
\direkt nach den öffnenden"""entfernt dort den Zeilenumbruch, und der Text beginnt in der nächsten Zeile. - Die Einrückung bleibt, wie sie geschrieben ist. Deshalb stehen die Zeilen hier ganz links.
- Anführungszeichen darin werden nicht maskiert.
- Der f-String füllt
{name}und{total:.2f}aus. Die geschweiften Klammern des Textes selbst werden doppelt geschrieben:{{und}}.
Genauer, für Neugierige
Im Rumpf einer Funktion sind die Zeilen eingerückt. dedent aus dem Modul textwrap entfernt die gemeinsame Einrückung links, wie der Textblock in Java:
from textwrap import dedent
def receipt(): return dedent("""\ Kaffeebohnen 18.50 € Honig 12.00 € """)
print(receipt(), end="")Kaffeebohnen 18.50 €Honig 12.00 €In Java lässt var den Compiler den Typ der Variable ermitteln. In Python schreibst du den Typ einer Variable nicht: Jede Variable ist wie var, und der Typ gehört zum Wert, nicht zum Namen.
prices = {"Kaffee": 18.50, "Honig": 12.00}for name, price in prices.items(): print(f"{name}: {price} €")Kaffee: 18.5 €Honig: 12.0 €Übung
Modernes Python
Löse die Aufgaben in deinem Editor.
1. Formen. Drei Klassen mit@dataclass(frozen=True): Circle mit radius, Rectangle mit width und height und Triangle mit base und height, dazu der Hinweis Shape = Circle | Rectangle | Triangle. Schreib area(shape) mit match und Klassenmustern, mit einem case _, der einen TypeError auslöst. Schreib auch name(shape), das für ein Rechteck mit gleichen Seiten „Quadrat“ sagt (mit einem if-Guard). Gib einen Kreis mit Radius 1, ein Rechteck 2 mal 3, ein Quadrat 4 mal 4, ein Dreieck mit Grundseite 3 und Höhe 5 und die Gesamtfläche aus.Kreis: 3.14Rechteck: 6.00Quadrat: 16.00Dreieck: 7.50Gesamt: 32.64@dataclass(frozen=True): ToOffice mit office, ToAddress mit city und weight_kg und Pickup ohne Felder. Der Preis: in eine Filiale 4,50 €; an eine Adresse 5,90 € in Berlin und 6,90 € anderswo, plus 1 € für jedes Kilogramm über 5; Abholung im Laden 0 €. Gib jede Lieferung (mit dem __repr__ der Dataclass) und ihren Preis aus.ToOffice(office='Hamburg Zentrum') -> 4.50 €ToAddress(city='Berlin', weight_kg=2) -> 5.90 €ToAddress(city='Hamburg', weight_kg=8) -> 9.90 €Pickup() -> 0.00 €Point mit @dataclass(frozen=True) und den Feldern x und y, und zähle in einem Dictionary mit visits.get(p, 0) + 1, wie oft er an jedem Punkt war. Schlag am Ende Point(2, 3) im Dictionary nach.Point(x=2, y=3): 3Point(x=5, y=1): 2Point(x=0, y=0): 1Besuche bei (2, 3): 3Weitere Aufgaben
- Ein Schüler als Dataclass.
Studentmitnameundgradeund einem__post_init__, das eine Note außerhalb von 2 bis 6 ablehnt. Sortiere eine Liste von Schülern mitsorted(students, key=lambda s: s.grade). - Wochentag. Schreib die Aufgabe „Wochentag“ aus der Lektion „Bedingungen“ mit
matchneu:case 1 | 2 | 3 | 4 | 5: return "Werktag". - Ein Brief. Mach mit Text zwischen
"""und einem f-String einen Brief, der eine Reservierung bestätigt: Name, Datum, Anzahl der Gäste und Preis in Euro.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: dict und set · Hashcodes und Hashfunktionen · Lambdas · List Comprehensions und Generatoren
- Geht weiter in: Entwurfsmuster · Datenbanken · Observer · Factory Method und Simple Factory
- Aufgaben zum Thema: Modernes Python