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