De geldige toestand van objecten bewaken · oefening
Valideer bij het aanmaken
Het schrijven van problems(question) bracht de regels op één plek, maar liet een gat: iets moet eraan denken die functie aan te roepen. Een vraag die niemand controleert, is net zo kapot als daarvoor en het programma blijft draaien totdat de schade ergens anders zichtbaar wordt.
Er is een moment waarop elke gewone aanmaakaanroep door hetzelfde stukje code gaat: __init__ wordt uitgevoerd voordat de aanroep het nieuwe object aan de aanroeper teruggeeft. Dat maakt het de natuurlijke plek om een ongeldig object te weigeren.
Weiger in plaats van terug te geven
In Python-basis I wierp je met raise een fout op voor ongeldige gegevens:
if points <= 0:
raise ValueError("points must be greater than zero")
Dezelfde instructie werkt binnen __init__:
De geldige vraag wordt zoals gewoonlijk gebouwd. Probeer nu een ongeldige:
Question("", "3", 2)
Python veroorzaakt een ValueError, de constructoraanroep geeft geen Question terug en de toewijzing vindt nooit plaats. De aanroeper ontvangt een geldig object of een exceptie. In de figuur loopt constructorinvoer via validatie naar een geldig object, of via de fout naar een exceptie voordat de aanroeper een object ontvangt. “Validate first, store second” betekent: valideer eerst, sla daarna op. “Invalid input” en “Valid input” zijn ongeldige en geldige invoer. “No invalid object returned” zegt dat er geen ongeldig object wordt teruggegeven; “Question object exists” zegt dat het vraagobject bestaat.

Controleer eerst en wijs daarna toe
Die volgorde is bewust gekozen en verdient een eigen uitspraak: valideer voordat je opslaat.
class Question:
def __init__(self, prompt, answer, points):
self.prompt = prompt # stored
if not prompt: # checked afterwards
raise ValueError("a question needs a prompt")
Deze versie werpt dezelfde fout op, dus een snelle test zou haar correct kunnen noemen. Toch slaat ze toestand op voordat ze bepaalt of die geldig is. In een grotere initialisatiemethode zou een latere hulpfunctie of een samenwerkend object die gedeeltelijke inrichting kunnen zien vóór de fout. Elke voorgestelde waarde eerst controleren geeft de klasse één betrouwbare regel: opgeslagen toestand is geldige toestand.
Wat laat Question("", "3", 2) achter wanneer __init__ een ValueError opwerpt?
Een fout waarmee de aanroeper verder kan
ValueError is hier de juiste keuze, omdat de aanroeper iets van het juiste type heeft meegegeven met een onbruikbare waarde. Python-basis I introduceerde die exceptie voor precies die situatie en dezelfde redenering geldt binnen een klasse.
Het bericht is net zo belangrijk als het exceptietype. Vergelijk:
raise ValueError("invalid")
raise ValueError("points must be greater than zero")
Het eerste vertelt de lezer van de traceback dat er iets verkeerd was. Het tweede vertelt wat er moet veranderen. Je schrijft dat bericht voor iemand die deze code niet kan zien, mogelijk jezelf over zes maanden.
Het bericht teruglezen
Python-basis I ving excepties op type op. Een foutafhandeling kan de opgevangen exceptie ook een naam geven en daarna lezen:
as error koppelt het exceptieobject aan een naam binnen de foutafhandeling. Het afdrukken ervan toont het bericht dat je schreef toen je de exceptie opwierp. Zo controleer je of een bericht het lezen waard is.
Dat is voorlopig alles wat as doet. Hoofdstuk 12 komt terug op wat een exceptie verder bevat.
Fouten zijn niet altijd het antwoord
Eén eerlijke kanttekening voordat je zelf aan de slag gaat. Een fout opwerpen vanuit __init__ is juist wanneer het object nooit bruikbaar zou kunnen zijn, zoals bij een vraag zonder antwoord.
Het is niet juist voor elke ongewenste waarde. Als een programma vragen leest uit een bestand waarvan bekend is dat enkele rijen onvolledig zijn, kan het verkeerd zijn het hele programma bij de eerste slechte rij te stoppen. Misschien kan het beter die rijen overslaan en aan het einde een aantal melden. In dat geval vangt de aanroeper de fout op en beslist die, wat een onderwerp voor hoofdstuk 12 is.
Weigeren een kapot object te bouwen blijft de juiste standaardkeuze. Het plaatst de fout waar de vergissing werd gemaakt, in plaats van drie functies verderop.
Opdracht
Maak __init__ af zodat een Question weigert te bestaan tenzij alle drie de regels gelden: een niet-lege vraagtekst, een niet-leeg antwoord en punten groter dan nul.
Gooi ValueError op met een bericht dat de geschonden regel benoemt. Controleer elke regel voordat je een attribuut toewijst.
Het programma onderaan maakt één goede vraag en probeert daarna drie ongeldige vragen, waarbij het van elke vraag het bericht toont. Voer het uit en lees wat elke fout meldt.