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.
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 attributen | Een echte beslissing |
|---|---|
__init__ die elk attribuut toewijst | Welke attributen bestaan |
__repr__ die ze opsomt | Of de klasse überhaupt een waarde is |
__eq__ die ze vergelijkt | Of punten positief moeten zijn |
| Veldvolgorde in die methoden | Wat 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:
Resultbewaart drie waarden en vergelijkt er twee.Durationbewaart twee waarden en toont er één.AttemptSummaryis 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.