Cursus
Iterators zijn objecten waar je overheen kunt itereren. Ze zijn een vast onderdeel van de programmeertaal Python en handig weggestopt voor gebruik in loops en list comprehensions. Elk object dat een iterator kan opleveren, noemen we een iterable.
Er komt nogal wat kijken bij het construeren van een iterator. Zo moet de implementatie van elk iteratorobject bestaan uit een __iter__()- en __next__() -methode. Naast deze vereisten moet de implementatie ook een manier hebben om de interne status van het object bij te houden en een StopIteration-exceptie op te werpen zodra er geen waarden meer kunnen worden teruggegeven. Deze regels staan bekend als het iteratorprotocol.
Je eigen iterator implementeren is een tijdrovend proces en niet altijd nodig. Een eenvoudiger alternatief is een generatorobject gebruiken. Generators zijn een speciaal soort functie die met het sleutelwoord yield een iterator teruggeven waar je één waarde per keer overheen kunt itereren.
Het vermogen om te herkennen wanneer je beter een iterator kunt implementeren of juist een generator kunt gebruiken, scherpt je vaardigheden als Python-programmeur. In de rest van deze tutorial leggen we de verschillen tussen beide objecten uit, zodat je in verschillende situaties de beste keuze kunt maken.
Woordenlijst
|
Term |
Definitie |
|
Iterable |
Een Python-object waar je in een lus overheen kunt lopen of over kunt itereren. Voorbeelden van iterables zijn lijsten, sets, tuples, dictionaries, strings, enz. |
|
Iterator |
Een iterator is een object waar je overheen kunt itereren. Iteratoren bevatten dus een telbaar aantal waarden. |
|
Generator |
Een speciaal type functie die niet één waarde retourneert, maar een iteratorobject met een reeks waarden. |
|
Lazy evaluation |
Een evalueringsstrategie waarbij bepaalde objecten pas worden geproduceerd wanneer ze nodig zijn. Daarom wordt lazy evaluation in sommige ontwikkelaarskringen ook wel “call-by-need” genoemd. |
|
Iteratorprotocol |
Een set regels die gevolgd moeten worden om in Python een iterator te definiëren. |
|
next() |
Een ingebouwde functie die wordt gebruikt om het volgende item uit een iterator terug te geven. |
|
iter() |
Een ingebouwde functie die wordt gebruikt om een iterable om te zetten in een iterator. |
|
yield() |
Een Python-sleutelwoord dat lijkt op return, behalve dat |
Python-iterators & iterables
Iterables zijn objecten die hun elementen één voor één kunnen teruggeven – je kunt erover itereren. Populaire ingebouwde Python-datastructuren zoals lijsten, tuples en sets zijn iterables. Andere datastructuren zoals strings en dictionaries worden ook als iterables beschouwd: een string kan over zijn tekens worden geïtereerd, en de sleutels van een dictionary kun je ook itereren. Als vuistregel geldt: elk object waar je in een for-lus overheen kunt itereren, is een iterable.
Python-iterables verkennen met voorbeelden
Gegeven de definities kunnen we concluderen dat alle iterators ook iterables zijn. Maar niet elke iterable is per se een iterator. Een iterable levert pas een iterator op wanneer erover wordt geïtereerd.
Om deze functionaliteit te demonstreren, maken we een lijst aan (een iterable) en produceren we een iterator door de ingebouwde functie iter() op de lijst aan te roepen.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
Hoewel de lijst zelf geen iterator is, zet aanroepen van de functie iter() haar om in een iterator en retourneert het iteratorobject.
Om te laten zien dat niet alle iterables iterators zijn, maken we dezelfde lijst aan en proberen we de functie next() aan te roepen, die wordt gebruikt om het volgende item uit een iterator terug te geven.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
In de code hierboven zie je dat een poging om next() op de lijst aan te roepen een TypeError oplevert – leer meer over exceptions en foutafhandeling in Python. Dit gedrag treedt op omdat een lijstobject een iterable is en geen iterator.
Python-iterators verkennen met voorbeelden
Als het doel dus is om over een lijst te itereren, dan moet eerst een iteratorobject worden gemaakt. Pas daarna kunnen we de iteratie door de waarden van de lijst beheren.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python maakt automatisch een iteratorobject aan wanneer je probeert te loopen over een iterable object.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
Wanneer de StopIteration-exceptie wordt opgevangen, eindigt de lus.
De waarden uit een iterator kunnen alleen van links naar rechts worden opgehaald. Python heeft geen previous()-functie om ontwikkelaars achteruit door een iterator te laten gaan.
Het luie karakter van iterators
Het is mogelijk om meerdere iterators te definiëren op basis van hetzelfde iterable object. Elke iterator behoudt zijn eigen voortgangsstatus. Door dus meerdere iteratorinstanties van een iterable te definiëren, kun je het einde van de ene instantie bereiken terwijl de andere nog aan het begin staat.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Merk op dat iterator_b het eerste element van de reeks print.
We kunnen dus zeggen dat iterators een lui karakter hebben: wanneer een iterator wordt gecreëerd, worden de elementen pas geleverd wanneer erom wordt gevraagd. Met andere woorden, de elementen van onze lijst worden pas teruggegeven zodra we er expliciet om vragen met next(iter(list_instance)).
Toch kunnen alle waarden uit een iterator in één keer worden opgehaald door een ingebouwde container voor iterables (bijv. list(), set(), tuple()) op het iteratorobject aan te roepen, waardoor de iterator gedwongen wordt al zijn elementen in één keer te genereren.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Dit is niet aan te raden voor grote iterators, omdat je zo elk element in één keer laat genereren en in het geheugen vasthouden, wat het doel van lazy evaluation tenietdoet.
Als een dataset te groot is om comfortabel in het geheugen te passen, of wanneer je luie iteratie wilt zonder een volledige iteratorclass te schrijven, is een generator meestal geschikter.
Python-generators
Het snelste alternatief voor het implementeren van een iterator is een generator gebruiken. Hoewel generators op gewone Python-functies lijken, zijn ze anders. Om te beginnen retourneert een generatorobject geen items. In plaats daarvan gebruikt het het sleutelwoord yield om items on the fly te genereren. We kunnen dus zeggen dat een generator een speciaal soort functie is die gebruikmaakt van lazy evaluation.
Generators slaan hun inhoud niet in het geheugen op, zoals je van een typische iterable zou verwachten. Als het doel bijvoorbeeld is om alle delers van een positief geheel getal te vinden, zouden we doorgaans een traditionele functie implementeren (leer meer over Python-functies in deze tutorial) als volgt:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
Bovenstaande code retourneert de volledige lijst met delers. Let echter op het verschil wanneer een generator wordt gebruikt in plaats van een traditionele Python-functie:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Omdat we het sleutelwoord yield in plaats van return gebruikten, wordt de functie na het uitvoeren niet afgesloten. In essentie hebben we Python opgedragen een generatorobject te maken in plaats van een traditionele functie, waardoor de status van het generatorobject kan worden bijgehouden.
Daardoor is het mogelijk om de functie next() op de luie iterator aan te roepen om de elementen van de reeks één voor één te tonen.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Een andere manier om een generator te maken is met een generatorcomprehension. Generator-expressies gebruiken een vergelijkbare syntaxis als een list comprehension, behalve dat ze ronde haakjes gebruiken in plaats van vierkante.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Het sleutelwoord yield in Python verkennen
Het sleutelwoord yield bepaalt de uitvoering van een generatorfunctie. In plaats van de functie af te sluiten zoals bij return, geeft yield de functie terug maar onthoudt het de status van de lokale variabelen.
De generator die door de yield-aanroep wordt teruggegeven, kan aan een variabele worden toegewezen en met de functie next() worden doorlopen – dit voert de functie uit tot aan het eerste yield-sleutelwoord dat het tegenkomt. Zodra yield wordt bereikt, wordt de uitvoering van de functie gepauzeerd. Op dat moment wordt de status van de functie opgeslagen. Daardoor kunnen we de functie-uitvoering hervatten wanneer we willen.
De functie gaat verder vanaf de aanroep naar yield. Bijvoorbeeld:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
In de code hierboven heeft onze generator vier yield-aanroepen, maar we proberen er vijf keer next op aan te roepen, wat een StopIteration-exceptie oplevert. Dit gebeurt omdat onze generator geen oneindige reeks is, dus hem vaker aanroepen dan verwacht put de generator uit.
Samenvatting
Samengevat: iterators zijn objecten waar je overheen kunt itereren, en generators zijn speciale functies die gebruikmaken van lazy evaluation. Als je je eigen iterator implementeert, moet je een __iter__()- en __next__()-methode maken, terwijl je een generator kunt implementeren met het sleutelwoord yield in een Python-functie of -comprehension.
Je kunt een aangepaste iterator verkiezen boven een generator wanneer je een object nodig hebt met complex statusbeheer of wanneer je andere methoden wilt aanbieden dan __next__(), __iter__() en __init__(). Een generator kan daarentegen de voorkeur hebben bij grote datasets, omdat ze hun inhoud niet in het geheugen opslaan, of wanneer het niet nodig is om een iterator te implementeren.

FAQS
Wat is het verschil tussen een iterator en een generator in Python?
Een iterator is elk object dat __iter__() en __next__() implementeert. Een generator is een eenvoudigere manier om een iterator te maken met een functie die het sleutelwoord yield gebruikt. Alle generators zijn iterators, maar niet alle iterators zijn generators.
Wanneer moet ik een generator gebruiken in plaats van een lijst in Python?
Gebruik een generator voor grote of oneindige reeksen, of wanneer geheugenefficiëntie belangrijk is. Lijsten houden alle elementen tegelijk in het geheugen, terwijl generators één waarde per keer produceren. Voor kleine datasets die je opnieuw gebruikt, is een lijst meestal prima.
Wat doet het sleutelwoord yield in Python?
Het sleutelwoord yield verandert een functie in een generator. In plaats van te retourneren en te stoppen, pauzeert yield de functie, geeft een waarde terug en onthoudt zijn status zodat de uitvoering bij de volgende aanroep kan worden hervat.
Hoe maak je een generator in Python?
Schrijf ofwel een functie die yield gebruikt in plaats van return, of gebruik een generatorexpressie — dezelfde syntaxis als een list comprehension maar met ronde haakjes, zoals (x * 2 for x in range(10)).
Zijn generators sneller dan iterators in Python?
Niet in ruwe snelheid, maar ze zijn wel geheugenefficiënter omdat ze waarden on demand produceren. Voor grote datasets levert dat vaak betere algehele prestaties op; voor kleine is het verschil verwaarloosbaar.
