0%

Kapitel 8 · Übung

Dataclasses und Wertobjekte

Wiederholten Klassencode erkennen

Hier ist die vollständige und korrekte Klasse Points vom Ende von Kapitel 7:

class Points:
    def __init__(self, amount):
        if amount <= 0:
            raise ValueError("points must be greater than zero")
        self._amount = amount

    @property
    def amount(self):
        return self._amount

    def __repr__(self):
        return f"Points({self.amount!r})"

    def __eq__(self, other):
        if not isinstance(other, Points):
            return NotImplemented

        return self.amount == other.amount

    def __hash__(self):
        return hash(self.amount)

    def __lt__(self, other):
        if not isinstance(other, Points):
            return NotImplemented
        return self.amount < other.amount

Mehr als zwanzig Zeilen, um zu sagen: „Ein Points ist ein positiver, schreibgeschützter Betrag.“

Manche Zeilen sind vorhersehbare Mechanik: das deklarierte Feld speichern, anzeigen und vergleichen. Positivität, Schreibschutz, Hashing und Ordnung sind eigene Entwurfszusagen. Eine Dataclass kann den vorhersehbaren Teil erzeugen. Diese Zusagen nimmt sie dir aber nicht ab.

Dieselbe Form, zweimal

Sieh dir an, was passiert, wenn ein Programm mehrere kleine Wertklassen hat:

class Points:
    def __init__(self, amount):
        self.amount = amount

    def __repr__(self):
        return f"Points({self.amount!r})"

    def __eq__(self, other):
        if not isinstance(other, Points):
            return NotImplemented
        return self.amount == other.amount


class Duration:
    def __init__(self, seconds):
        self.seconds = seconds

    def __repr__(self):
        return f"Duration({self.seconds!r})"

    def __eq__(self, other):
        if not isinstance(other, Duration):
            return NotImplemented
        return self.seconds == other.seconds

Lies beide nebeneinander. Ändere drei Namen, und es ist dieselbe Klasse. Bei Duration wurde nichts neu überlegt, nur getippt oder, wahrscheinlicher, kopiert und angepasst.

Warum kopierter Code schiefgeht

Der Aufwand liegt nicht im Tippen. Die Kopien entwickeln sich auseinander.

Try it

Zwei Ergebnisse mit unterschiedlichen Gesamtpunktzahlen, die als gleich gemeldet werden. Die repr-Darstellung unterstützt die falsche Aussage, weil auch sie total auslässt.

Jemand hat total zu __init__ hinzugefügt und die beiden Methoden darunter nicht angepasst. Das ist weniger Unachtsamkeit als eine Frage der Anzahl: Drei Stellen mussten geändert werden, zwei wurden übersehen. Bei acht Attributen und sechs Klassen stellt sich nicht mehr die Frage, ob das irgendwann passiert.

Was ist in der obigen Klasse Result schiefgegangen?

Was „Boilerplate“ bedeutet

Solcher Code hat einen Namen: Boilerplate. Damit ist nicht schlechter Code gemeint, sondern Code, dessen Inhalt vollständig aus etwas folgt, das du bereits festgelegt hast. Kannst du aus der Attributliste jede Zeile vorhersagen, vermitteln diese Zeilen keine neue Information. Sie existieren nur, weil die Sprache sie verlangt.

Die richtige Antwort auf Boilerplate ist nicht, schneller zu tippen. Frage stattdessen, ob etwas diesen Code erzeugen kann, sodass du nichts mehr aufeinander abstimmen musst.

Nicht alles davon ist Boilerplate

Unterscheide genau, welche Teile dazugehören. Die nächste Lektion erzeugt genau diese und nicht mehr:

Durch die Attribute bestimmtBewusst entschieden
__init__ weist jedes zuWelche Attribute existieren
__repr__ listet sie aufOb die Klasse überhaupt ein Wert ist
__eq__ vergleicht sieOb Punkte positiv sein müssen
Die Feldreihenfolge in diesen MethodenWas is_correct bedeutet

Die linke Spalte können die Standardeinstellungen einer Dataclass für dich schreiben. Die rechte Spalte ist dein Entwurf. Kein Werkzeug entscheidet sie. Hashing, Ordnung, Einfrieren und Validierung verlangen ausdrückliche Entscheidungen, die später im Kapitel folgen.

Die Übung lässt dich das Auseinanderlaufen in einem kleinen Programm finden, bevor du die Funktion kennenlernst, die es verhindert. Wenn du genau weißt, wozu die Mechanik dient, wirkt die nächste Lektion nicht wie Magie.

Aufgabe

Drei Klassen wurden hier von Hand geschrieben, und jede passt nicht mehr vollständig zu ihren eigenen Attributen.

Repariere alle drei so, dass jedes in __init__ zugewiesene Attribut auch in __repr__ und __eq__ vorkommt:

  • Result speichert drei Werte und vergleicht zwei.

  • Duration speichert zwei Werte und zeigt einen.

  • AttemptSummary ist eine Momentaufnahme als Wertobjekt, die drei Werte speichert. Ihr __eq__ fehlt vollständig.

Folge dem vorhandenen repr-Format der Klassen: ClassName(value, value), wobei jeder Wert so geschrieben wird, wie er im Code erscheinen würde.

Das Programm am Ende gibt Paare aus, die nicht gleich sein sollten. Wenn du fertig bist, sollen die drei Vergleichszeilen False melden. Die letzten beiden Zeilen sollen in den Objektdarstellungen jeden gespeicherten Wert zeigen.