mitmario.dev

Datenklassen

Python Sandbox 3 Min Lesezeit 4 BeispieleLektion 4 von 6

Der Artikel mit Namen, Preis und Bestand ist eine Klasse, die vor allem Daten trägt. Für so eine Klasse schreibt man in Python erstaunlich viel, und zwar immer wieder dasselbe. Eine Sache fehlt trotzdem noch.

Gleich, aber nicht dasselbe

Zwei gleiche Artikel
class Artikel:
    def __init__(self, name, preis, bestand=0):
        self.name = name
        self.preis = preis
        self.bestand = bestand

    def __repr__(self):
        return f"Artikel({repr(self.name)}, {self.preis}, {self.bestand})"


brot = Artikel("Brot", 2.49, 12)
noch_ein_brot = Artikel("Brot", 2.49, 12)
print(brot, noch_ein_brot)
print(brot == noch_ein_brot)
print(brot == brot)

Zwei Brote mit demselben Namen, demselben Preis und demselben Bestand, und == sagt False. Ohne weitere Angabe fragt == bei Objekten nur, ob es dasselbe Objekt ist. Ob zwei Objekte dieselben Werte haben, ist dabei egal. Das Brot ist sich selbst gleich, dem zweiten Brot nicht. Für einen Artikel, der nur aus seinen Werten besteht, ist das die falsche Antwort.

Beheben ließe sich das mit einer weiteren Methode mit Unterstrichen, __eq__. Dann stünde aber jedes Attribut an vier Stellen in der Klasse, nämlich als Parameter von __init__, in der Zuweisung an self, in __repr__ und im Vergleich. Bei drei Attributen geht das noch. Bei acht vergisst man irgendwo eins.

@dataclass

Dieselbe Klasse als Datenklasse
from dataclasses import dataclass


@dataclass
class Artikel:
    name: str
    preis: float
    bestand: int = 0


brot = Artikel("Brot", 2.49, 12)
butter = Artikel("Butter", 2.29)
print(brot)
print(butter)
print(brot == Artikel("Brot", 2.49, 12))

brot.bestand = brot.bestand - 3
print(brot.bestand)

Bei einer Datenklasse (dataclass) entstehen diese Methoden von selbst. In der Klasse stehen nur noch die Felder (fields), also die Attribute, die jedes Objekt bekommt. Jedes hat einen Typhinweis wie in Lektion 8.6, und ein Standardwert geht wie bei einem Parameter: bestand: int = 0.

Daraus wird ein __init__ mit genau diesen Parametern in genau dieser Reihenfolge, dazu ein __repr__, der jedes Feld beim Namen nennt, und ein ==, das die Werte vergleicht. Die Butter ohne Bestand hat 0 Stück. Das Brot ist einem neuen Brot mit denselben Werten gleich, und geändert wird ein Feld wie jedes andere Attribut.

@dataclass über der Klasse ist ein Dekorator (decorator). Damit bekommt die Klasse etwas angehängt, das sie sonst nicht könnte. Mehr braucht es hier nicht, das @ kommt bei pytest und bei Flask wieder.

Die Typhinweise prüft Python übrigens auch hier nicht. Artikel("Brot", "2,49") legt einfach einen Artikel an, dessen Preis ein Text ist. Erst eine Rechnung damit geht schief, denn mal zwei ergibt 2,492,49. Die Typhinweise sagen, was gemeint ist, für jeden, der die Klasse liest. Auch für dich selbst in drei Monaten.

Ohne Typhinweis kein Feld

Ohne Typhinweis kein Feld
from dataclasses import dataclass


@dataclass
class Artikel:
    name: str
    preis: float
    bestand = 0


brot = Artikel("Brot", 2.49, 12)
print(brot)

Beim Bestand fehlt diesmal : int. Ohne Typhinweis ist bestand = 0 kein Feld, sondern ein gewöhnlicher Wert der Klasse, und __init__ kennt nur zwei Parameter. Auf drei Werte antwortet Python deshalb mit TypeError: Artikel.__init__() takes 3 positional arguments but 4 were given. Gezählt wird wie bei den Methoden mit self. Erlaubt sind also drei, und mit self waren es vier.

Ob etwas ein Feld ist, hängt in einer Datenklasse also am Typhinweis. Fehlt dagegen der ganze Wert und steht nur bestand da, meldet Python schon beim Lesen der Klasse NameError: name 'bestand' is not defined.

Nach einem Feld sortieren

Ein Regal sortieren
from dataclasses import dataclass


@dataclass
class Artikel:
    name: str
    preis: float
    bestand: int = 0


regal = [
    Artikel("Milch", 1.19, 30),
    Artikel("Brot", 2.49, 12),
    Artikel("Butter", 2.29, 8),
]
for artikel in sorted(regal, key=lambda artikel: artikel.preis):
    print(artikel)

print(max(regal, key=lambda artikel: artikel.bestand).name)

Sortieren geht mit dem key aus Lektion 8.7. sorted(regal) allein endet mit TypeError: '<' not supported between instances of 'Artikel' and 'Artikel'. Ob die Milch vor dem Brot kommt, weiß Python nicht, es könnte ja nach jedem Feld gehen. Mit key=lambda artikel: artikel.preis ist es gesagt.

max mit demselben Muster findet den Artikel mit dem größten Bestand. Zurück kommt dabei der ganze Artikel und nicht nur die Zahl. Deshalb steht .name dahinter.

Was die Aufgabe will

Eine Zeile aus dem Kontoauszug ist bisher ein Dictionary mit Text darin. Mit einer Datenklasse wird aus jeder Zeile eine Buchung, mit einem echten Datum und einem echten Betrag.

Zum Mitnehmen

Mit @dataclass entstehen __init__, __repr__ und der Vergleich mit == aus den Feldern der Klasse. Ein Feld ist nur, was einen Typhinweis hat, etwa bestand: int = 0 mit Standardwert. Sortiert wird eine Liste solcher Objekte mit key=lambda artikel: artikel.preis, dann stehen sie in der Reihenfolge der Preise.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.