2.31. Iterators en generators

De for-lus heeft meer werk verricht dan het lijkt. Deze pagina behandelt het iteratorprotocol waarop ze draait, en het sleutelwoord yield waarmee je je eigen iterators kunt bouwen.

2.31.1. Het iteratorprotocol

Elk object waarover kan worden geïtereerd, implementeert twee methoden:

  • __iter__() – retourneert een iterator over de items van het object.

  • __next__() – op de iterator, retourneert het volgende item of werpt StopIteration op wanneer er geen meer zijn.

De ingebouwde iter() roept __iter__ aan; next() roept __next__ aan. Loop handmatig door een lijst:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

for is suiker voor “roep __iter__ eenmaal aan, loop daarna op __next__ totdat StopIteration.”

Wat for x in items: daadwerkelijk doet:

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Elke lijst, tuple, string, dict, set, bestandsobject en generator implementeert al __iter__ en __next__ – wat de reden is waarom ze allemaal werken met for.

2.31.2. yield en generatorfuncties

Een functie die een yield-statement bevat, is een generatorfunctie. Het aanroepen ervan voert het lichaam niet uit; het retourneert een generator-object (een iterator) dat het lichaam één yield tegelijk uitvoert:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Uitvoer:

0
1
2

Elke aanroep van next() hervat de functie tot de volgende yield, geeft die waarde aan de aanroeper en pauzeert daar. De lokale toestand (i in dit geval) blijft behouden tussen hervattingen.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

next() voert het lichaam uit tot de volgende yield, geeft de waarde terug en pauzeert. De lokale toestand overleeft de pauze.

Generators zijn de eenvoudigste manier om lui een reeks te produceren – er wordt geen lijst opgebouwd, items worden pas berekend wanneer de consument erom vraagt, en de functie kan eindeloos items opleveren als ze dat wil.

2.31.3. Luie pijplijnen

Generators laten zich goed combineren. De uitvoer van de ene generator kan een andere voeden:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

De waarden stromen één voor één door de pijplijn – geen tussenliggende lijst, geen ingebouwde bovengrens in numbers, en de consument (for v in pipeline) bepaalt wanneer er gestopt wordt.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

Elke next() op de consument activeert één pull door de keten; waarden bestaan pas wanneer iets erom vraagt.

2.31.3.1. yield from

Een lus die items uit een ander iterable trekt en elk ervan oplevert, komt vaak genoeg voor dat Python een snelkoppeling biedt. De expressie yield from iter levert elke waarde op die het iterable produceert, in volgorde – alsof de generator een for x in iter: yield x-lus inline had:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Uitvoer:

1
2
3
4
5
a
b
c

yield from is exact equivalent aan de expliciete for-lus, alleen korter, en het propageert StopIteration van het binnenste iterable netjes omhoog naar de buitenste generator – handig bij het achter elkaar koppelen van meerdere generators.

2.31.3.2. Wanneer yield opraakt

Voorbij het einde van een generatorfunctie vallen (of een expliciete return raken) werpt automatisch StopIteration op. Het is niet nodig om die handmatig op te werpen; de omliggende for-lus ziet het en eindigt.

Gebruik generators wanneer de producerende code van nature als een lus met een paar yield-punten is geschreven; gebruik een gewone list comprehension wanneer je werkelijk de hele reeks in het geheugen nodig hebt.