0%

De geldige toestand van objecten bewaken · oefening

Bereken informatie met properties

attempt.current_score() beschermt de score. Het maakt de klasse ook iets minder prettig te gebruiken dan de versie met de fout, en daar mogen we eerlijk over zijn:

print(attempt.score)            # was: readable, and dangerously assignable
print(attempt.current_score())  # is:  safe, and now a function call

Elk programma dat attempt.score gebruikte, moet veranderen en elke toekomstige lezer moet onthouden welke waarden op dit object gewone attributen zijn en welke methodeaanroepen. Dat is een echte prijs die iedereen die de klasse gebruikt betaalt om een probleem binnen de klasse op te lossen.

Python biedt een manier om die prijs te vermijden.

Een attribuut dat code uitvoert

Een property is een methode die aanroepers zonder ronde haken bereiken, alsof het een attribuut is:

Try it

Weer attempt.score, zonder ronde haken. De regel @property boven de methode maakt dat mogelijk. Python ziet de toegang, voert de methode uit en geeft terug wat die oplevert.

De regel met @ heet een decorator. Later maak je kennis met decorators als algemeen idee. Behandel @property voorlopig als de schrijfwijze voor deze mogelijkheid.

Wat dat opleverde

De interface werd weer zoals vroeger en de bescherming bleef:

attempt.score = 500

Toewijzen aan een property die alleen een getter definieert, veroorzaakt een AttributeError. De tegenstrijdigheid waarmee het hoofdstuk begon, is nu echt niet meer mogelijk, in plaats van alleen ontmoedigd door een underscore.

Nog interessanter: de klasse kan nu helemaal anders over _score gaan denken.

Try it

Er is nu helemaal geen attribuut _score meer. De score wordt elke keer dat erom wordt gevraagd uit de opgeslagen resultaten berekend en elk programma dat attempt.score gebruikt, blijft zonder enige wijziging werken. Dat is de opbrengst van de scheiding tussen interface en representatie uit de vorige les.

De tweede Attempt slaat nooit een score op. Waarom kan attempt.score toch worden gelezen?

Een opgeslagen waarde kan verouderen

Op verzoek berekenen voorkomt een soort fout die makkelijk ontstaat en moeilijk opvalt. Bekijk een versie die een lopend totaal bewaart en antwoorden laat verwijderen:

def undo_last(self):
    self._results.pop()          # the results changed
                                 # and _score did not

Met een opgeslagen _score moet die methode eraan denken af te trekken. Zodra iemand dat vergeet, meldt het object een score die niet bij zijn eigen resultaten past. Met een berekende property is undo_last compleet zoals die is geschreven: de volgende uitlezing van score weerspiegelt de werkelijkheid, omdat ze daaruit wordt afgeleid.

Daar staat echt werk bij elke uitlezing tegenover. Voor een som over een handvol resultaten stelt dat niets voor. Als een berekening duur is en voortdurend wordt opgevraagd, kan opslaan redelijk zijn. De klasse beheert dan de taak om de waarde correct te houden. Geef de voorkeur aan berekenen totdat je een gemeten reden hebt om dat niet te doen.

Wanneer gebruik je geen property?

Een property moet voelen alsof je een object iets over zichzelf vraagt. Lezen moet snel zijn en niemand verrassen.

Schrijf een gewone methode als het werk langzaam is, een netwerk benadert of iets verandert tijdens het lezen. attempt.score suggereert een feit over het object. attempt.recalculate_from_server() suggereert een bewerking. Een bewerking achter attribuutsyntaxis verbergen is hoe mensen eindigen met een lus die ongemerkt duizend verzoeken doet.

Opdracht

De uitgewerkte Attempt berekent score en response_count al uit de ene lijst _results. Voeg de resterende alleen-lezen property toe: accuracy, het aandeel antwoorden dat punten opleverde, als getal tussen 0.0 en 1.0.

Een poging zonder antwoorden heeft een nauwkeurigheid van 0.0. Bepalen hoe je die deling afhandelt, is het belangrijkste deel van de oefening.

Voer het programma uit en vergelijk de drie gegevens na undo_last(). Het laatste antwoord was onjuist: door het te verwijderen blijft de score op 2, daalt het aantal van 2 naar 1 en stijgt de nauwkeurigheid van 0.5 naar 1.0. Geen enkele regel in undo_last hoeft die totalen afzonderlijk bij te werken.