Dataclasses en waardeobjecten · oefening
Klasseattributen en instantieattributen
Elk attribuut dat je tot nu toe hebt geschreven werd toegewezen in __init__ of vermeld in een dataclass, en elk object kreeg een eigen exemplaar. Er is een tweede plek waar een attribuut kan staan: bij de klasse in plaats van bij een object.
Eén waarde voor de hele klasse
default_points wordt toegewezen in de klasse-inhoud, niet in __init__. Er is er precies één, gedeeld door elke Question, en je kunt die via de klasse zelf lezen zonder dat er een object bestaat.
Instantieattributen beantwoorden ‘wat geldt voor dit ene object?’ Klasseattributen beantwoorden ‘wat geldt voor elk object van deze soort?’
Lezen, en de verrassing
Lezen werkt via beide. Toewijzen doet niet wat het lijkt te doen:
5, 1, 1.
Toewijzen via een object veranderde het klasseattribuut niet. Het maakte een nieuw instantieattribuut op first dat het klasseattribuut overschaduwt. second en de klasse blijven onaangeraakt en first heeft nu een eigen waarde die de klasse niet meer zal volgen.
Wijs via de klasse toe om het voor iedereen te veranderen:
first meldt nog steeds 5, omdat het nu een eigen waarde heeft. second meldt 3, omdat het nog steeds de klasse leest. Het diagram laat de gedeelde klassewaarde en de eigen waarde van een instantie zien. ‘One class attribute, two readers’ betekent ‘één klasseattribuut, twee lezers’. ‘Assigning through first shadows it’ zegt dat toewijzen via first het klasseattribuut overschaduwt. ‘Reads’ betekent ‘leest’ en ‘still reads’ betekent ‘leest nog steeds’.

Waarom meldt second.default_points na first.default_points = 5 nog steeds de oude waarde?
De regel die problemen geeft
Die regel is meestal vooral opmerkelijk. Het wordt een fout als het klasseattribuut veranderbaar is:
De eindtoets bevat de vraag van de herhaling. self.questions.append(...) heeft nooit iets toegewezen, dus er werd geen instantieattribuut gemaakt: de aanroep vond de ene gedeelde lijst en voegde daaraan toe.
Dit is dezelfde valkuil als de veranderbare standaardwaarde uit les 4, maar via een andere route. De oplossing volgt hetzelfde idee: geef elk object een eigen exemplaar.
class Quiz:
def __init__(self, title):
self.title = title
self.questions = []
Gebruik in een dataclass field(default_factory=list). Een veranderbaar klasseattribuut is gedeelde toestand. Gebruik dat alleen als het delen bewust is en de klasse er duidelijke bewerkingen voor beheert; collecties per object horen op elke instantie.
Waar klasseattributen wel goed voor zijn
Als je ze voor hun eigenlijke doel gebruikt, zijn ze nuttig en veilig. Constanten die bij de klasse horen:
MAX_POINTS is een gegeven over vragen in het algemeen. Het in de klasse bewaren is daarom duidelijker dan een variabele op moduleniveau, en het is overal leesbaar als Question.MAX_POINTS. De naam in hoofdletters is het gebruikelijke signaal dat de waarde niet bedoeld is om te veranderen.
Merk op wat @dataclass met die twee regels deed: niets. Ze hebben geen annotatie, dus het zijn geen velden, ze verschijnen niet in __init__ of de repr en ze worden niet vergeleken. Voor de syntaxis in dit hoofdstuk is een geannoteerde naam een veld en een naam zonder annotatie een gewoon klasseattribuut. Geavanceerde annotaties zoals typing.ClassVar kunnen een geannoteerd klasseattribuut expliciet markeren, maar dat valt buiten deze les.
Geen MAX_POINTS in de repr, precies zoals bedoeld.
De oefening gebruikt klasseattributen voor de twee dingen waar ze goed in zijn en herstelt er één die werd gebruikt voor iets waar ze slecht in zijn.
Opdracht
Gebruik klasseattributen waarvoor ze goed zijn en stop ze te gebruiken waarvoor ze niet geschikt zijn.
Question moet MIN_POINTS = 1 en MAX_POINTS = 10 als klasseattributen bevatten, zonder annotatie, zodat ze buiten de gegenereerde __init__, repr en gelijkheid blijven. Gebruik ze in __post_init__ om punten buiten het inclusieve bereik te weigeren. Houd ook de vraagtekst en het antwoord niet-leeg.
Quiz bevat een fout: _questions is een veranderbaar klasseattribuut, dus elke quiz deelt één lijst. Geef elke quiz een eigen interne lijst _questions, weiger None in add en geef een kopie terug uit questions().
QuizAttempt moet tellen hoeveel pogingen zijn gemaakt met een klasseattribuut count dat __init__ via de klasse verhoogt. Elke poging moet hetzelfde totaal melden, omdat het een gegeven is over pogingen in het algemeen en niet over één ervan.
Voer het programma uit. De eindtoets mag de herhalingsvragen niet bevatten, een vraag buiten het bereik moet worden geweigerd en beide pogingen moeten hetzelfde aantal melden.