Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus den letzten Lektionen: eine normale Klasse mit __init__. Was gibt das aus?
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(1, 2) == Point(1, 2))// I2.5 · ca. 35 Min. · Aufbau
Nach dieser Lektion ersetzen Sie selbst geschriebenes __init__ und __repr__ durch @dataclass, geben Feldern sichere Standardwerte, prüfen Werte in __post_init__ und machen Instanzen unveränderlich und sortierbar.
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(1, 2) == Point(1, 2))Vorhersagen · Aufgabe 2 von 7
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p, p == Point(1, 2))Üben · Aufgabe 3 von 7
from dataclasses import dataclass, field
@dataclass
class Basket:
items: list[str] = ____(default_factory=list)Üben · Aufgabe 4 von 7
from dataclasses import dataclass, field
@dataclass
class Rect:
w: int
h: int
area: int = field(init=False)
def __post_init__(self):
self.area = self.w * self.h
print(Rect(2, 3))Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
from dataclasses import dataclass
@dataclass(frozen=True)
class Team:
name: str
members: list
t = Team("A", [])
t.members.append("ada")
print(t)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
Drei Datenklassen arbeiten zusammen. Money ist eingefroren und geordnet, Beträge lassen sich also vergleichen, in Sets verwenden und nie versehentlich ändern. LineItem prüft seine Menge in __post_init__. Order beginnt dank default_factory mit einer eigenen leeren Liste. Keine der Klassen hat ein selbst geschriebenes __init__ oder __repr__.
main.py
from dataclasses import dataclass, field
@dataclass(frozen=True, order=True)
class Money:
cents: int
currency: str = "EUR"
def text(self) -> str:
return f"{self.cents / 100:.2f} {self.currency}"
@dataclass
class LineItem:
name: str
price: Money
quantity: int = 1
def __post_init__(self) -> None:
if self.quantity < 1:
raise ValueError(f"quantity must be at least 1, got {self.quantity}")
def total(self) -> Money:
return Money(self.price.cents * self.quantity, self.price.currency)
@dataclass
class Order:
customer: str
items: list[LineItem] = field(default_factory=list)
def add(self, name: str, cents: int, quantity: int = 1) -> None:
self.items.append(LineItem(name, Money(cents), quantity))
def total(self) -> Money:
return Money(sum(item.total().cents for item in self.items))
order = Order("ada")
order.add("coffee", 850, 2)
order.add("mug", 1200)
print(order.items[1])
print(order.total().text(), Order("bob").items)
print(max(item.total() for item in order.items).text())
print(Money(500) == Money(500), len({Money(500), Money(500), Money(200)}))
try:
order.add("tea", 420, 0)
except ValueError as err:
print("refused:", err)
Ausführen mit
python main.pyAusgabe
LineItem(name='mug', price=Money(cents=1200, currency='EUR'), quantity=1)
29.00 EUR []
17.00 EUR
True 2
refused: quantity must be at least 1, got 0Tab 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 2
Schreiben Sie Contact als Datenklasse mit den Feldern name, email und tags um; tags ist standardmäßig für jeden Kontakt eine neue leere Liste. Entfernen Sie in __post_init__ Leerzeichen am Rand der E-Mail-Adresse, schreiben Sie sie klein und lösen Sie ValueError aus, wenn sie kein @ enthält. tag(label) fügt ein Label nur hinzu, wenn es noch fehlt. Der Starter teilt eine Liste zwischen allen Kontakten: Die Tests bemerken das.
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.
Importieren Sie dataclass und field, setzen Sie @dataclass über die Klasse und ersetzen Sie __init__ durch drei annotierte Felder.
tags: list[str] = field(default_factory=list) gibt jedem Kontakt eine neue Liste.
Weisen Sie in __post_init__(self) -> None zuerst self.email = self.email.strip().lower() zu und prüfen Sie dann auf "@".
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from dataclasses import dataclass, field
@dataclass
class Contact:
name: str
email: str
tags: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
self.email = self.email.strip().lower()
if "@" not in self.email:
raise ValueError(f"not an email address: {self.email!r}")
def tag(self, label: str) -> None:
if label not in self.tags:
self.tags.append(label)
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
class Contact:
def __init__(self, name: str, email: str, tags: list[str] = []) -> None:
self.name = name
self.email = email
self.tags = tags
def tag(self, label: str) -> None:
self.tags.append(label)
test_main.py
from main import Contact
def test_repr():
"""Die Datenklasse zeigt ihre Felder"""
got = repr(Contact("Ada", "ada@example.com"))
assert got == "Contact(name='Ada', email='ada@example.com', tags=[])", f"repr ist {got}"
def test_equal():
"""Zwei Kontakte mit denselben Daten sind gleich"""
assert Contact("Ada", "ada@example.com") == Contact("Ada", "ada@example.com"), "gleiche Kontakte gelten als verschieden"
def test_own_tags():
"""Jeder Kontakt hat seine eigene Tag-Liste"""
a = Contact("Ada", "ada@example.com")
b = Contact("Bob", "bob@example.com")
a.tag("vip")
assert b.tags == [], f"das Taggen von a hat auch b.tags geändert, zu {b.tags!r}"
def test_tag_once():
"""Ein Tag wird nur einmal gespeichert"""
c = Contact("Ada", "ada@example.com")
c.tag("vip")
c.tag("vip")
assert c.tags == ["vip"], f"nach zweimal vip ist tags {c.tags!r}"
def test_normalises_email():
"""Die E-Mail-Adresse wird bereinigt und kleingeschrieben"""
got = Contact("Ada", " Ada@Example.COM ").email
assert got == "ada@example.com", f"email ist {got!r}"
def test_rejects_email():
"""Eine E-Mail-Adresse ohne @ löst ValueError aus"""
try:
Contact("Ada", "ada.example.com")
except ValueError:
pass
else:
assert False, 'Contact("Ada", "ada.example.com") 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 2 von 2
Als Text sortiert "1.9.3" nach "1.10.0". Machen Sie Version zu einer eingefrorenen, geordneten Datenklasse mit major, minor und patch (beide mit Standardwert 0) und geben Sie ihr ein __str__, das "1.10.0" zurückgibt. parse_version(text) macht aus "1.10.0" Version(1, 10, 0). newest(texts) gibt die neueste Version als String zurück und vergleicht Zahlen, nicht Text.
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.
Ändern Sie den Decorator in @dataclass(frozen=True, order=True). Verglichen wird dann Feld für Feld: zuerst major, dann minor, dann patch.
Teilen Sie in parse_version den Text an "." und wandeln Sie jeden Teil mit int() um.
newest kann jeden Text parsen, max() der Version-Objekte nehmen und str() des Ergebnisses zurückgeben.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int = 0
patch: int = 0
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"
def parse_version(text: str) -> Version:
major, minor, patch = (int(part) for part in text.split("."))
return Version(major, minor, patch)
def newest(texts: list[str]) -> str:
return str(max(parse_version(text) for text in texts))
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
@dataclass
class Version:
major: int
minor: int = 0
patch: int = 0
def parse_version(text: str) -> Version:
return Version(0)
def newest(texts: list[str]) -> str:
return max(texts)
test_main.py
import dataclasses
from main import Version, newest, parse_version
def test_parse():
"""parse_version liest drei Zahlen"""
got = parse_version("1.10.3")
assert got == Version(1, 10, 3), f'parse_version("1.10.3") ist {got!r}'
def test_str():
"""str() liefert die Form mit Punkten"""
got = str(Version(2, 0, 1))
assert got == "2.0.1", f"str(Version(2, 0, 1)) ist {got!r}"
def test_order():
"""Versionen werden Zahl für Zahl verglichen"""
assert Version(1, 10, 0) > Version(1, 9, 3), "Version(1, 10, 0) ist nicht größer als Version(1, 9, 3)"
def test_newest():
"""newest vergleicht Zahlen, nicht Text"""
got = newest(["1.9.3", "1.10.0", "1.2.0"])
assert got == "1.10.0", f"newest lieferte {got!r}"
def test_hashable():
"""Gleiche Versionen zählen im Set einmal"""
got = len({parse_version("1.2.0"), parse_version("1.2.0"), parse_version("1.2.1")})
assert got == 2, f"das Set enthält {got} Versionen, erwartet sind 2"
def test_frozen():
"""Eine Version lässt sich nicht ändern"""
v = Version(1, 2, 3)
try:
v.major = 9 # type: ignore[misc]
except dataclasses.FrozenInstanceError:
pass
else:
assert False, "v.major = 9 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 herunterladenfrom dataclasses import dataclass
@dataclass
class Cart:
owner: str
items: list = []
Was Python ausgibt
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factoryWarum, und die Lösung
Ein Standardwert wird einmal ausgewertet, also würden alle Carts dieselbe Liste teilen, und was in einen Warenkorb kommt, stünde in allen. @dataclass fängt das für Listen, Dicts und Sets ab und scheitert schon beim Definieren der Klasse. Schreiben Sie items: list = field(default_factory=list) und importieren Sie field aus dataclasses.
from dataclasses import dataclass
@dataclass
class Order:
express: bool = False
customer: str
print(Order("ada"))
Was Python ausgibt
TypeError: non-default argument 'customer' follows default argument 'express'Warum, und die Lösung
Das erzeugte __init__ nimmt die Felder in der Reihenfolge, in der sie stehen, und ein Parameter ohne Standardwert darf nicht auf einen mit Standardwert folgen, genau wie bei einem normalen def. Stellen Sie die Felder mit Standardwert ans Ende: zuerst customer: str, dann express: bool = False.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
visited = {Point(0, 0), Point(1, 2)}
print(len(visited))
Was Python ausgibt
TypeError: cannot use 'Point' as a set element (unhashable type: 'Point')Warum, und die Lösung
Eine Datenklasse mit erzeugtem __eq__ hat keinen Hash, weil sich ihre Felder ändern könnten, nachdem sie in einem Set liegt. Ändern sich die Punkte nie, schreiben Sie @dataclass(frozen=True): Eingefrorene Instanzen bekommen einen Hash aus ihren Feldern und funktionieren in Sets und als Dict-Schlüssel.
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.