← Python/Eindproject: Urenrapport
Les 3 van 8
MODULE 14 · 03◷ 35 min

Een registratie als gecontroleerde waarde

Na deze les kun je
  • Samenhangende projectgegevens in een frozen dataclass bewaren.
  • Invarianten bij de constructie controleren, ook buiten de tekstparser.
  • Een kalenderdag onderscheiden van een tijdstip.

Een parser controleert tekst, maar andere code kan ook rechtstreeks een registratie aanmaken. Denk aan een test, een import uit een andere bron of een toekomstige koppeling. De afspraken over een registratie horen daarom ook bij het model zelf.

Een waarde met samenhang

Een registratie bevat een kalenderdag, een team en een aantal minuten. Losse lijsten met dezelfde posities maken die betekenis minder zichtbaar. Met de dataclass Session krijgen de velden vaste namen en kan andere code lezen wat zij voorstelt.

Dataclassgelijkheid gebruikt de velden
from dataclasses import dataclass

@dataclass(frozen=True)
class Position:
    row: int
    column: int

first = Position(2, 4)
second = Position(2, 4)
print(first == second)
print(first is second)
Verwachte uitvoer
True
False

Het voorbeeld herhaalt het verschil tussen gelijkheid en identiteit. Twee afzonderlijke objecten kunnen dezelfde gegevens vertegenwoordigen. Dataclassgelijkheid vergelijkt die velden, zodat je niet zelf bij iedere vergelijking alle attributen hoeft na te lopen.

Voor ons urenrapport blijven dubbele registraties wel twee registraties. Waardegelijkheid is op zichzelf geen opdracht om invoer te dedupliceren. Later telt de rekenmodule iedere ontvangen registratie opnieuw mee.

Een invariant geldt na iedere constructie

Een invariant is een afspraak die voor ieder geldig modelobject hoort te gelden. Een Session heeft een bruikbare dag, een bekende teamnaam en een duur binnen de grenzen. De bron waaruit het object afkomstig is, verandert die afspraken niet.

Dataclassannotaties leggen de bedoelde typen vast voor de lezer en gereedschappen. Zij valideren de ontvangen waarden niet automatisch. Gebruik daarom __post_init__ om de afspraken direct na de gegenereerde constructor te controleren.

Wanneer een controle faalt, werp je de meegeleverde InputError op. De constructor levert dan geen half bruikbaar object aan de aanroeper. De foutmelding vertelt welke invoerafspraak nog niet klopt.

Een kalenderdag is een bewuste keuze

Het rapport groepeert registraties op dag. Tijdstippen, tijdzones en daggrenzen van een klok behoren niet tot dit gegevensmodel. require_day bewaart daarom een echte datumwaarde en weigert een datetime.

Een subtype kan meer informatie dragen
from datetime import date, datetime

day = date(2026, 10, 2)
moment = datetime(2026, 10, 2, 12, 30)
print(isinstance(day, date))
print(isinstance(moment, date))
print(isinstance(moment, datetime))
Verwachte uitvoer
True
True
True

De uitvoer laat zien waarom alleen isinstance(value, date) niet voldoende is voor deze afspraak. Een datetime valt ook onder date. Je moet die extra subtypefamilie dus bewust uitsluiten wanneer het contract alleen kalenderdagen toelaat.

Een eigen subclass van date mag wel als kalenderdag worden gebruikt. Je maakt daarvan geen nieuw object zonder reden: require_day geeft de gecontroleerde waarde zelf terug. Zo blijft de controle één kleine herbruikbare grens.

Omgezette waarden blijven omgezet

De parser normaliseert de teamtekst en maakt van het minutenveld een geheel getal. Het model verwacht die interne waarden al te ontvangen. Een team met hoofdletters of een tekst zoals "45" wordt hier dus niet nogmaals gerepareerd.

Bij minuten sluit je bool afzonderlijk uit. True hoort in Python bij de integerfamilie, maar betekent geen gewerkte minuut in dit project. Een gewone int-subklasse blijft wel een geheel getal; de waarde moet daarna nog tussen één en 1440 liggen.

Veelgemaakte fout: na controle vrij blijven wijzigen

Als een ander onderdeel later eenvoudig een onbekend team kan toewijzen, kan een gecontroleerd object zijn afspraak alsnog verliezen. Met frozen=True houdt de dataclass gewone toewijzingen aan velden tegen. Die leveren FrozenInstanceError op en bewaren de bestaande waarden.

Deze velden bevatten zelf onveranderlijke datum-, tekst- en getalwaarden. Daardoor past een bevroren dataclass hier goed bij het begrip registratie. Voor gecorrigeerde gegevens maak je een nieuwe waarde, zodat een eerder gebruikt object niet ongemerkt verandert.

AAN DE SLAG 0/4
1

Drie waarden horen bij elkaar

Maak Session een dataclass met day, team en minutes, in die volgorde. Bewaar de werkelijk meegegeven waarden en gebruik de gewone dataclassgelijkheid voor gelijke registraties. TEAMS blijft de meegeleverde verzameling balie, bouw en groen.

2

Een dag als expliciete invariant

Maak require_day(value). Geef dezelfde waarde terug als zij een date is maar geen datetime. Anders geef je InputError("datum moet een kalenderdatum zijn"). Een date-subklasse is toegestaan.

3

Iedere constructie blijft geldig

Controleer in __post_init__ de dag via require_day. team moet al een bekende genormaliseerde str zijn, anders InputError("onbekend team"). minutes is een int, met int-subklassen toegestaan maar bool uitgesloten, anders InputError("minuten moeten een geheel getal zijn"). Alleen 1–1440 is geldig, anders InputError("minuten moeten tussen 1 en 1440 liggen").

4

Een registratie blijft dezelfde waarde

Gebruik dataclass(frozen=True). Gewone toewijzingen aan day, team of minutes moeten FrozenInstanceError geven en alle oorspronkelijke velden behouden.

Samenvatting

  • Een model bewaart samenhangende waarden met een duidelijke betekenis.
  • __post_init__ controleert invarianten bij iedere normale constructie.
  • Frozen velden helpen een eenmaal gecontroleerde registratie stabiel te houden.

Hierna gebruikt de CSV-lezer dit model voor iedere ontvangen gegevensrij.

hours/model.py
UitvoerPython
›
›_

Hier komt je resultaat.

Voer je code uit en ontdek
wat je hebt gemaakt.

0/4 stappen
EVEN MEEKIJKEN

Bekijk hoe het kan.

Je mag de oplossing bekijken. Deze poging telt dan niet mee als afgeronde les. Met Reset kun je daarna opnieuw zelf oefenen.