Zum Inhalt springen
aviral gupta

// I2.5 · ca. 35 Min. · Aufbau

Datenklassen

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.

Lektion 5 von 6 in I2 Klassen

Danach können Sie

  • Eine Datenklasse mit Feldern und Standardwerten definieren und ihr erzeugtes __init__, __repr__ und __eq__ nutzen
  • field(default_factory=...) für Listen als Standardwert und __post_init__ für Prüfungen und abgeleitete Felder nutzen
  • frozen=True und order=True einsetzen und Vergleichen, Sortieren und Hashen vorhersagen
  1. 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))
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: dieselbe Klasse als Datenklasse. Was gibt das aus?

    from dataclasses import dataclass
    
    
    @dataclass
    class Point:
        x: int
        y: int
    
    
    p = Point(1, 2)
    print(p, p == Point(1, 2))
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Funktion, die jedem Basket eine eigene leere Liste gibt.

    from dataclasses import dataclass, field
    
    
    @dataclass
    class Basket:
        items: list[str] = ____(default_factory=list)
    = (default_factory=list)
  4. Üben · Aufgabe 4 von 7

    area wird in __post_init__ berechnet. Was gibt das aus?

    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))
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Teil einer Datenklasse zu, was er bewirkt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Team ist eingefroren. Was gibt das aus?

    from dataclasses import dataclass
    
    
    @dataclass(frozen=True)
    class Team:
        name: str
        members: list
    
    
    t = Team("A", [])
    t.members.append("ada")
    print(t)
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie eine Datenklasse Item mit den Feldern name: str, price: float und quantity: int = 1. Lösen Sie in __post_init__ einen ValueError aus, wenn price negativ ist. Ergänzen Sie eine Methode total(), die price * quantity zurückgibt. Item("pen", 1.5, 4) wird als Item(name='pen', price=1.5, quantity=4) mit total 6.0 ausgegeben, und Item("pen", -1) wird abgelehnt.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

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

Eine Bestellung mit Positionen und Geldbeträgen

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.py

Ausgabe

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 0
  • Das repr eines LineItem enthält das repr des Money darin.
  • Order("bob").items ist leer: Jede Bestellung bekommt über default_factory ihre eigene Liste.
  • max() funktioniert mit Money wegen order=True; verglichen wird zuerst cents, dann currency.
  • Das Set enthält zwei Beträge: Eingefrorene Money-Instanzen sind hashbar, und gleiche zählen einmal.
Ändern und ausführen

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.

Übungen

Übung 1 von 2

Ein Kontakt als Datenklasse

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.

Hinweise
  1. Hinweis 1

    Importieren Sie dataclass und field, setzen Sie @dataclass über die Klasse und ersetzen Sie __init__ durch drei annotierte Felder.

  2. Hinweis 2

    tags: list[str] = field(default_factory=list) gibt jedem Kontakt eine neue Liste.

  3. Hinweis 3

    Weisen Sie in __post_init__(self) -> None zuerst self.email = self.email.strip().lower() zu und prüfen Sie dann auf "@".

Eine Lösung zeigen

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)
Auf dem eigenen Computer ausführen

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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Versionsnummern, die richtig sortieren

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.

Hinweise
  1. Hinweis 1

    Ändern Sie den Decorator in @dataclass(frozen=True, order=True). Verglichen wird dann Feld für Feld: zuerst major, dann minor, dann patch.

  2. Hinweis 2

    Teilen Sie in parse_version den Text an "." und wandeln Sie jeden Teil mit int() um.

  3. Hinweis 3

    newest kann jeden Text parsen, max() der Version-Objekte nehmen und str() des Ergebnisses zurückgeben.

Eine Lösung zeigen

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))
Auf dem eigenen Computer ausführen

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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Eine Liste als Standardwert

from 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_factory

Warum, 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.

Ein Feld ohne Standardwert nach einem mit Standardwert

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.

Eine normale Datenklasse in ein Set stecken

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

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Annotierte Klassenvariablen werden Felder

@dataclass liest den Klassenrumpf und macht jede Klassenvariable mit Typannotation zu einem Feld. Aus den Feldern schreibt es __init__, __repr__ und __eq__ für Sie, in der Reihenfolge der Felder. Ein Feld mit Wert, etwa quantity: int = 1, bekommt einen Standardwert, und Felder mit Standardwert müssen nach denen ohne stehen. Eine Klassenvariable ohne Annotation ist kein Feld. Die Annotationen werden zur Laufzeit nicht geprüft: Point("a", "b") wird angenommen.

Sichere Standardwerte und __post_init__

Eine Liste, ein Dict oder ein Set als Standardwert würde von allen Instanzen geteilt, deshalb lehnt @dataclass items: list = [] mit ValueError ab. Schreiben Sie stattdessen items: list[str] = field(default_factory=list): Die Factory wird für jede neue Instanz aufgerufen. Nachdem das erzeugte __init__ die Felder gesetzt hat, ruft es __post_init__ auf, falls die Klasse eine solche Methode hat. Dort prüfen Sie Werte, lösen ValueError aus, bereinigen Werte oder berechnen ein Feld mit field(init=False), das kein Parameter von __init__ ist.

frozen und order

Das erzeugte __eq__ vergleicht die Felder der Reihe nach wie Tupel, und nur Instanzen derselben Klasse. Mit order=True kommen <, <=, > und >= hinzu, die genauso vergleichen, sodass sorted() und max() funktionieren. frozen=True sorgt dafür, dass eine Zuweisung an ein Feld FrozenInstanceError auslöst. Eine eingefrorene Klasse mit eq bekommt auch __hash__, ihre Instanzen passen also in Sets und dienen als Dict-Schlüssel; eine normale Datenklasse ist nicht hashbar. frozen wirkt nur flach: Eine Liste in einer eingefrorenen Instanz kann sich weiter ändern.

Quellen

Zuletzt geprüft am 29. September 2026