0%

Hoofdstuk 8 · oefening

Dataclasses en waardeobjecten

Herken herhalende klassecode

Hier is de klasse Points van het einde van hoofdstuk 7, volledig en correct:

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

Meer dan twintig regels om te zeggen: ‘een Points is een positieve, alleen-lezen hoeveelheid’.

Sommige regels zijn voorspelbaar mechanisch werk: het gedeclareerde veld opslaan, weergeven en vergelijken. Positiviteit, alleen-lezen toegang, hashing en ordening zijn afzonderlijke ontwerpafspraken. Een dataclass kan het voorspelbare deel genereren, maar neemt die beslissingen niet voor je.

Dezelfde vorm, twee keer

Kijk wat er gebeurt als een programma meerdere kleine waardeklassen heeft:

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

Lees die twee naast elkaar. Verander drie namen en het is dezelfde klasse. Over niets in Duration is nagedacht; het is getypt, of waarschijnlijker gekopieerd en aangepast.

Waarom gekopieerde code fout gaat

Het probleem is niet het typen. Het is dat kopieën uit elkaar gaan lopen.

Try it

Twee resultaten met verschillende totalen, gemeld als gelijk. De repr ondersteunt de onjuiste indruk, omdat die total ook weglaat.

Iemand voegde total toe aan __init__ en werkte de twee methoden eronder niet bij. Dat is niet zozeer slordigheid als een kwestie van aantallen: drie plekken moesten veranderen en twee werden gemist. Met acht attributen en zes klassen is het niet meer de vraag óf dit gebeurt.

Wat ging er fout in de klasse Result hierboven?

Wat ‘boilerplate’ betekent

Code zoals deze heeft een naam: boilerplate. Geen slechte code, maar code waarvan de inhoud volledig wordt bepaald door iets wat je al hebt gezegd. Als een lezer elke regel kan voorspellen uit de lijst met attributen, voegen die regels geen informatie toe en bestaan ze alleen omdat de taal ze vereist.

De juiste reactie op boilerplate is niet die sneller schrijven. Het is vragen of iets die kan genereren, zodat je niets gelijk hoeft te houden.

Niet alles is boilerplate

Wees precies over welke onderdelen eronder vallen, want de volgende les genereert precies deze en niet meer:

Bepaald door de attributenEen echte beslissing
__init__ die elk attribuut toewijstWelke attributen bestaan
__repr__ die ze opsomtOf de klasse überhaupt een waarde is
__eq__ die ze vergelijktOf punten positief moeten zijn
Veldvolgorde in die methodenWat is_correct betekent

De linkerkolom is wat de standaardinstellingen van dataclasses voor je kunnen schrijven. De rechterkolom is jouw ontwerp en geen hulpmiddel zal dat bepalen. Hashing, ordening, bevriezen en validatie vereisen expliciete keuzes die later in het hoofdstuk aan bod komen.

De oefening vraagt je de afwijkingen in een klein programma te vinden voordat je de voorziening leert die ze voorkomt. Precies weten waarvoor het mechanische werk dient, voorkomt dat de volgende les als magie voelt.

Opdracht

Drie klassen zijn hier met de hand geschreven en elke klasse is gaan afwijken van de eigen attributen.

Herstel alle drie, zodat elk attribuut dat in __init__ wordt toegewezen ook in __repr__ en in __eq__ voorkomt:

  • Result bewaart drie waarden en vergelijkt er twee.

  • Duration bewaart twee waarden en toont er één.

  • AttemptSummary is een waardemomentopname die drie waarden bewaart, en de __eq__ ontbreekt volledig.

Volg de repr-opmaak die de klassen al gebruiken: ClassName(value, value), met elke waarde geschreven zoals die in code zou verschijnen.

Het programma onderaan drukt paren af die niet gelijk horen te zijn. Als je klaar bent, moeten de drie vergelijkingsregels False melden. De laatste twee regels moeten elke opgeslagen waarde in de objectrepresentaties tonen.