0%

Iteratie als interface · oefening

Maak een domeinobject iterable

Hoofdstuk 6 gaf Quiz een methode om de vragen beschikbaar te maken:

for question in quiz.questions():
    print(question.prompt)

Dat blijft nuttig als een aanroeper een momentopname in een lijst nodig heeft. Als aanroepers alleen hoeven te itereren, kan het object rechtstreeks iteratie bieden:

for question in quiz:
    print(question.prompt)

Delegeren aan iets dat al iterable is

Quiz bevat een lijst en een lijst weet al hoe die kan worden doorlopen. __iter__ hoeft dus niets te bedenken:

Try it

Eén regel en nergens een __next__. iter(self._questions) vraagt de lijst om een nieuwe iterator en geeft die terug. De quiz is daardoor zonder extra werk opnieuw doorloopbaar: elke ronde krijgt een eigen doorloper, om de redenen die les 3 uitlegde.

Dit schrijf je in de praktijk. De versie met twee klassen uit les 3 bestaat zodat je kunt zien wat iter() op die lijst doet; bijna elke echte __iter__ is deze ene regel.

Wat het oplevert

De winst is niet dat for question in quiz drie tekens korter is. De winst is dat Quiz nu werkt met alles wat het iteratieprotocol spreekt, zonder dat iets daarvan weet wat een Quiz is:

Try it

sorted, in, enumerate, zip, min, max, sum, any, all en het uitpakken van tuples kunnen allemaal een iterable gebruiken. Geen ervan is geschreven met Quiz in gedachten. Ze vragen om een iterator en Quiz antwoordt. De items moeten nog steeds bij de bewerking passen: deze quiz bevat bijvoorbeeld strings, dus je kunt de vragen sorteren maar niet optellen met sum.

Dat is de titel van dit hoofdstuk. Iteratie is een interface: implementeer één methode en een groot deel van de taal gaat je object accepteren.

Waarom kun je twee keer itereren over een Quiz waarvan __iter__ iter(self._questions) teruggeeft, terwijl dat bij de Countdown uit de vorige les niet kon?

Een kopie teruggeven, opnieuw bekeken

Hoofdstuk 6 gaf zorgvuldig list(self._questions) terug, zodat een aanroeper de quiz niet kon leegmaken via de ontvangen lijst. Opent __iter__ dat gat opnieuw?

Niet via de iterator zelf. Een iterator laat een aanroeper items één voor één ontvangen en het protocol biedt geen bewerking om items aan de bank toe te voegen of eruit te verwijderen. De opgeleverde objecten kunnen nog steeds veranderbaar zijn, dus for question in quiz beschermt de structuur van de collectie in plaats van diepe alleen-lezen toegang te beloven.

Dat maakt __iter__ hier een beter antwoord dan questions(): het biedt één natuurlijke stroom vragen zonder een lijst uit te delen waarvan je de lengte kunt opvragen, die je kunt indexeren of die je kunt wijzigen. Behoud een methode questions() alleen als een aanroeper die reeksachtige interface echt nodig heeft.

Kiezen waarover je itereert

Een object met meerdere collecties moet beslissen welke collectie for x in obj betekent en het antwoord moet voor de hand liggen. Een Quiz itereert over de vragen. Een Gradebook itereert over de registraties. Is er geen duidelijke keuze, definieer dan helemaal geen __iter__: geef de collecties namen en laat aanroepers for result in book.records() schrijven.

__iter__ belooft het doorlopen van één bepaald soort item. Het belooft geen indexering of lengte; dat zijn afzonderlijke reeksbewerkingen.

Opdracht

Maak beide klassen iterable door te delegeren aan de collectie die ze al bevatten.

Quiz.__iter__ doorloopt de vragen. Gradebook.__iter__ doorloopt de registraties. Elk bestaat uit één regel en geen van beide heeft een __next__ nodig.

Beide moeten opnieuw doorloopbaar blijven: twee keer itereren ziet alles twee keer.

Schrijf daarna twee functies die via het protocol werken in plaats van via een methodenaam:

  • total_points(quiz) telt de punten van elke vraag op en gebruikt daarbij de quiz rechtstreeks;

  • top_record(book) geeft de registratie met het hoogste percentage terug, of None als de resultatenlijst leeg is.

Geen van beide mag .questions() of .records() aanroepen. De beoordelaar geeft ze zowel een gewone lijst als een Quiz en een Gradebook, wat alleen werkt als ze om iteratie vragen en verder niets.