← Python/Idiomatisch en performance
Les 3 van 8
MODULE 13 · 03◷ 30 min

Alleen invoer lezen die je nodig hebt

Na deze les kun je
  • Een begrensd voorstuk zonder extra bronlezing opvragen.
  • Een transformatie pas bij het volgende verzoek uitvoeren.
  • Batches en lopende totalen als iterators samenstellen.

Een logbestand kan honderdduizend regels bevatten, terwijl je op dit moment alleen de eerste vijf meldingen nodig hebt. Als je alles eerst naar een lijst omzet, betaal je alvast voor werk dat je niet gebruikt. Met luie verwerking laat je de vraag van de afnemer bepalen hoeveel de bron levert.

Eén volgende waarde tegelijk

Een iterator onthoudt waar hij gebleven is en levert bij next hoogstens één volgende waarde. Je hebt generators al gebruikt bij bestanden. Lui betekent hier dat de volgende berekening of bronlezing pas gebeurt wanneer de afnemer daarom vraagt.

Dat is geen belofte dat iedere bewerking sneller wordt. Een generator heeft ook overhead, en sommige algoritmen hebben alle gegevens tegelijk nodig. Het voordeel is dat je over de hoeveelheid vooraf verricht werk kunt redeneren.

islice uit itertools begrenst hoeveel waarden je uit een iterator opvraagt. list(islice(source, 3)) maakt wel een lijst, maar alleen met de eerste drie waarden. De rest van de bron wordt niet alvast opgehaald.

De bron laat haar echte leesmomenten zien
from itertools import islice

def measurements():
    for value in [12, 15, 19]:
        print(f"lees {value}")
        yield value

source = measurements()
print("gemaakt")
print(list(islice(source, 2)))
Verwachte uitvoer
gemaakt
lees 12
lees 15
[12, 15]

De printregels van de bron laten zien wanneer er werkelijk een waarde wordt gevraagd. Het aanmaken van de generator print nog niets. Na twee verzoeken zijn ook maar twee bronwaarden voorbijgekomen.

Filteren en omvormen combineren

Een generatorfunctie met yield kan eerst een waarde beoordelen en daarna een omgevormde waarde leveren. Niet iedere gelezen bronwaarde hoeft dus een uitvoerwaarde te worden. Voor de volgende positieve waarde moet je misschien meerdere ongeschikte waarden passeren.

Vraag je maar één resultaat, dan hoort de generator na dat resultaat te pauzeren. Een lijstcomprehension leest de hele bron voordat de aanroeper iets ontvangt. Beide kunnen dezelfde uiteindelijke lijst maken, maar hebben een andere werkwijze onderweg.

Kleine groepen en tussenstanden

batched verzamelt maximaal n bronwaarden in een tuple. De laatste groep mag korter zijn. De functie leest geen volgende groep vooruit; nul of een negatieve groepsgrootte is ongeldig en geeft ValueError.

accumulate levert telkens het totaal tot en met de zojuist gelezen waarde. Met [3, 4, -2] krijg je zo 3, 7, 5. Zonder een expliciete beginwaarde verschijnt bij lege invoer geen totaal.

Een batch bewaart de eigen groep en accumulate de lopende toestand. Lui betekent dus niet dat er helemaal geen geheugen wordt gebruikt. Het betekent dat deze hulpmiddelen niet automatisch een lijst van alle toekomstige resultaten opbouwen.

Veelgemaakte fout: eerst alles verzamelen

Een generator om een al gemaakte lijst zetten maakt de eerdere bronlezing niet ongedaan. Bij iter(list(source)) is de volledige bron immers al verbruikt. Bij een grote of onbegrensde bron is dat precies wat je wilde vermijden.

Een iterator rond een lijst maakt het eerdere werk niet lui
events = []
def source():
    for value in [4, 5, 6]:
        events.append(value)
        yield value

eager = iter(list(source()))
print(events)
events.clear()
lazy = (value * 2 for value in source())
print(events)
print(next(lazy))
print(events)
Verwachte uitvoer
[4, 5, 6]
[]
8
[4]

De gebeurtenissen maken het verschil zichtbaar zonder kwetsbare milliseconden te vergelijken. De ene vorm vraagt alles vóór het eerste resultaat; de andere leest alleen voor dat resultaat. De controles in de oefening gebruiken dezelfde soort echte bronwaarneming.

De grens hoort bij het contract

De functies ontvangen een iterable die maar eenmaal doorlopen hoeft te kunnen worden. Gebruik daarom geen lengtecontrole, indexering of tweede volledige ronde over de bron. Positieve kwadraten slaan nul en negatieve getallen over; de overige functies behouden alle geleverde waarden.

AAN DE SLAG 0/4
1

Een begrensd voorstuk

Vul first_n(items, n) in voor een niet-negatieve n: geef een lijst met hoogstens de eerste n waarden. Lees geen extra waarde; n=0 mag de bron niet aanspreken.

2

Positieve kwadraten lui leveren

Vul positive_squares(items) in als generator: sla getallen <=0 over en yield de kwadraten van de overige getallen. Lees niets bij aanmaken en pauzeer direct na iedere geleverde waarde.

3

Tuples van een vaste bovengrens

Vul batches(items, size) in: geef een iterator die tuples van hoogstens size waarden levert, met een eventueel kortere laatste groep. Geen voorafgaande bronlezing; size<=0 geeft ValueError.

4

Tussenstanden zonder eindlijst

Vul running_totals(items) in: geef een iterator met het lopende opteltotaal. Lees pas wanneer een resultaat gevraagd wordt, lees geen waarde vooruit en lever niets voor een lege bron.

Samenvatting

  • Een iterator doet pas meer werk wanneer je een volgende waarde vraagt.
  • islice begrenst consumptie; batched groepeert; accumulate houdt een tussenstand bij.
  • Een gelijke eindlijst bewijst nog geen gelijke bronlezing.

Hierna geef je functies extra gedrag met een wrapper, zonder hun oorspronkelijke contract te verliezen.

lazy_tools.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.