Last chance! 50% off DataCamp Premium
Sale ends in
Python-functies hebben niet altijd een return-statement. Generatorfuncties zijn functies met het trefwoord yield in plaats van return.
Deze functies produceren generator-iterators, objecten die een gegevensstroom voorstellen. De elementen die door een iterator worden voorgesteld, worden pas gemaakt en geleverd wanneer dat nodig is. Dit type evaluatie wordt vaak lazy evaluation genoemd.
Bij het werken met grote datasets bieden generators een geheugenefficiënt alternatief voor het opslaan van gegevens in lijsten, tuples en andere datastructuren die voor elk element ruimte in het geheugen vereisen. Generatorfuncties kunnen ook oneindige iterators maken, wat niet mogelijk is met eager geëvalueerde structuren zoals lijsten en tuples.
Voordat we beginnen, frissen we kort de verschillen tussen functies en generators op:
|
Eigenschap |
Functie |
Generator |
|
Waardeproductie |
Retourneert alle waarden in één keer |
Levert waarden één voor één, op aanvraag |
|
Uitvoering |
Voert volledig uit voordat er iets wordt geretourneerd |
Pauzeert na yield en gaat verder wanneer de volgende waarde wordt opgevraagd |
|
Trefwoord |
return |
yield |
|
Geheugengebruik |
Mogelijk hoog, slaat de volledige reeks in het geheugen op |
Laag, bewaart alleen de huidige waarde en toestand voor de volgende |
|
Iteratie |
Meerdere iteraties mogelijk, maar vereist opslag van de volledige reeks |
Ontworpen voor single-pass iteratie, efficiënter voor grote of oneindige reeksen |
yield generatorfuncties makenDe term generator in Python kan verwijzen naar een generator-iterator of een generatorfunctie. Dit zijn verschillende maar verwante objecten in Python. In deze tutorial worden de volledige termen vaak gebruikt om verwarring te voorkomen.
Laten we eerst generatorfuncties verkennen. Een generatorfunctie lijkt op een gewone functie maar bevat het trefwoord yield in plaats van return.
Wanneer een Python-programma een generatorfunctie aanroept, maakt het een generator-iterator. Iterators leveren een waarde op aanvraag en pauzeren hun uitvoering totdat er een volgende waarde nodig is. Laten we een voorbeeld bekijken om dit concept uit te leggen en het verschil te tonen tussen gewone functies en generatorfuncties.
Laten we eerst een gewone functie definiëren met een return-statement. Deze functie accepteert een reeks woorden en een letter, en retourneert een lijst met het aantal voorkomens van die letter in elk woord:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
De functie geeft een lijst met 1, 3 en 0, omdat er één a in apple staat, drie a's in banana en geen in cherry. Dezelfde functie kan worden herschreven met list comprehensions in plaats van een lege lijst te initialiseren en .append() te gebruiken:
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
Deze gewone functie retourneert bij elke aanroep een lijst met alle resultaten. Als de woordenlijst echter groot is, legt het aanroepen van deze functie beslag op het geheugen, omdat het programma een nieuwe lijst maakt en opslaat die net zo groot is als de oorspronkelijke. Als deze functie herhaaldelijk met verschillende argumenten wordt gebruikt, of vergelijkbare functies andere bewerkingen op de oorspronkelijke gegevens uitvoeren, kan de druk op het geheugen snel toenemen.
In plaats daarvan kun je een generatorfunctie gebruiken:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
De functie bevat het yield-trefwoord in plaats van return. Deze generatorfunctie retourneert bij aanroep een generatorobject, dat wordt toegewezen aan output. Dit object is een iterator. Het bevat niet de gegevens met het aantal voorkomens van de letter in elk woord. In plaats daarvan maakt en levert de generator de waarden wanneer ze nodig zijn. Laten we de eerste waarde uit deze generator-iterator ophalen:
print(next(output))
1
De ingebouwde functie next() is één manier om de volgende waarde uit een iterator te halen. We bekijken later in deze tutorial nog andere manieren.
De code in de generatorfunctie wordt uitgevoerd tot het programma de regel met het trefwoord yield bereikt. In dit voorbeeld start de for-lus zijn eerste iteratie en haalt het eerste element uit de lijst words. De tekenreeks-methode .count() retourneert een geheel getal, hier 1, omdat er één a in apple voorkomt. De generator levert deze waarde, die wordt geretourneerd door next(output).
De generator output pauzeert nu zijn uitvoering. De generator heeft dus de eerste iteratie van de for-lus voltooid en het aantal voorkomens van de letter a in het eerste woord gevonden. Hij wacht nu tot hij weer nodig is.
Als de ingebouwde next() opnieuw met output als argument wordt aangeroepen, hervat de generator de uitvoering vanaf het punt waar hij gepauzeerd was:
print(next(output))
3
De generator gaat verder vanaf de regel met yield in de eerste iteratie van de for-lus. Omdat de for-lus geen verdere regels code heeft, keert hij terug naar het begin van de lus en haalt het tweede element uit de lijst words. De waarde die door .count() wordt geretourneerd, is hier 3, en deze waarde wordt geleverd. De generator pauzeert opnieuw op dit punt.
De derde aanroep van next() hervat deze uitvoering:
print(next(output))
0
De tweede iteratie bereikt het einde van de for-lus en gaat door naar de derde iteratie. De code komt opnieuw bij de regel met yield, ditmaal met de waarde 0 omdat er geen a in cherry voorkomt.
De generator pauzeert weer. Het programma bepaalt het vervolg van de generator pas wanneer we next() voor de vierde keer aanroepen:
print(next(output))
Traceback (most recent call last):
...
StopIteration
De uitvoering wordt hervat vanaf het einde van de derde iteratie van de for-lus. Maar de lus is klaar met itereren omdat er geen elementen meer in de lijst words zitten. De generator werpt een StopIteration-exceptie.
In de meeste gevallen worden de elementen van de generator niet direct met next() benaderd, maar via een ander iteratieproces. De StopIteration-exceptie signaleert het einde van het iteratieproces. We verkennen dit verder in de volgende sectie van deze tutorial.
Python heeft nog een manier om generator-iterators te maken wanneer hun werking met één expressie kan worden weergegeven, zoals in het vorige voorbeeld. De generator-iterator output kan worden gemaakt met een generator-expressie:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
De expressie tussen haakjes die aan output is toegewezen, is een generator-expressie, die een vergelijkbare generator-iterator maakt als die van de generatorfunctie find_letter_occurrences().
We sluiten deze sectie af met nog een voorbeeld van een generatorfunctie om te laten zien hoe de uitvoering pauzeert en hervat telkens wanneer er een element nodig is:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
Deze generatorfunctie heeft geen lus. In plaats daarvan bevat hij drie regels met het trefwoord yield. De code maakt een generator-iterator status wanneer de generatorfunctie show_status() wordt aangeroepen. De eerste keer dat het programma next(status) aanroept, start de generator de uitvoering. Hij print de tekenreeks "Start" en pauzeert na de eerste yield-expressie. De generator levert None, omdat er geen object volgt op het trefwoord yield.
Het programma print de tekenreeks "Middle" pas wanneer next() voor de tweede keer wordt aangeroepen:
next(status)
Middle
De generator pauzeert na de tweede yield-expressie. De derde aanroep van next() print de laatste tekenreeks, "End":
next(status)
End
De generator pauzeert bij de laatste yield-expressie. De volgende keer dat het programma een waarde uit deze generator-iterator opvraagt, wordt een StopIteration-exceptie opgegooid:
next(status)
Traceback (most recent call last):
...
StopIteration
We bekijken in de volgende sectie meer manieren om generators te gebruiken.
Generatorfuncties maken generator-iterators, en iterators zijn iterable. Laten we deze zin uitpakken. Elke keer dat het programma een generatorfunctie aanroept, wordt er een iterator gemaakt. Omdat iterators iterable zijn, kunnen ze in for-lussen en andere iteratieve processen worden gebruikt.
Daarom is de ingebouwde functie next() niet de enige manier om elementen in een iterator te benaderen. Deze sectie verkent andere manieren om met generators te werken.
Laten we een generatorfunctie uit een eerdere sectie van deze tutorial opnieuw bekijken:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
In plaats van next() meerdere keren te gebruiken, gebruikt deze versie van de code de generator-iterator output in een for-lus. Omdat iterators iterable zijn, kunnen ze in for-lussen worden gebruikt. De lus haalt items uit de generator-iterator totdat er geen waarden meer zijn.
In tegenstelling tot datastructuren zoals lijsten en tuples kan een iterator maar één keer worden gebruikt. De code print de waarden niet opnieuw als we proberen dezelfde for-lus een tweede keer uit te voeren:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
De iterator is uitgeput door de eerste for-lus en kan dus geen waarden meer leveren. Als de generator na uitputting opnieuw nodig is, moeten we een nieuwe generator-iterator maken vanuit de generatorfunctie.
Het is ook mogelijk om meerdere generator-iterators tegelijkertijd in een programma te hebben:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
De generatorfunctie find_letter_occurrences() maakt twee generator-iterators: first_output en second_output. Hoewel beide iterators verwijzen naar dezelfde gegevens in de lijst words, verlopen ze onafhankelijk van elkaar.
In dit voorbeeld halen we de eerste waarde uit first_output met next(). De generator-iterator levert 1 en pauzeert. Vervolgens doorloopt het programma second_output. Omdat deze generator nog geen waarden heeft geleverd, gaat de lus door alle waarden van de tweede iterator. Tot slot is er nog een for-lus die first_output doorloopt. Deze iterator heeft eerder in het programma al zijn eerste waarde geleverd. De lus doorloopt de resterende waarden van deze iterator.
De for-lus is niet het enige proces om door generator-iterators te itereren:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
In deze voorbeelden roept het programma de generatorfunctie direct aan om de generator-iterator te maken en te gebruiken in plaats van hem aan een variabele toe te wijzen. In het eerste voorbeeld wordt de iterator uitgepakt met de ster-notatie. Dit proces steunt op hetzelfde iteratieprotocol als de for-lus.
In het tweede voorbeeld wordt de generator-iterator doorgegeven aan de ingebouwde functie sorted(), die een iterable argument vereist. Generators zijn iterable en kunnen daarom overal worden gebruikt waar Python-iteratie plaatsvindt.
Een generator levert een waarde en pauzeert tot de volgende waarde nodig is. Telkens wanneer de code een waarde uit een iterator opvraagt, voert de code binnen de generatorfunctie uit tot de volgende yield-expressie wordt geëvalueerd. In alle voorbeelden in deze tutorial had de generatorfunctie tot nu toe een eindig aantal yield-expressies. Het is echter mogelijk om een generator te maken die een oneindig aantal waarden levert met behulp van een while-lus in de generatorfunctie. In het volgende voorbeeld levert de generator een willekeurige kleur uit de lijst met kleuren die aan de generatorfunctie wordt doorgegeven:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
De generatorfunctie get_color() heeft een yield-expressie binnen een while-lus. Daarom komt de code altijd weer een yield-expressie tegen wanneer de volgende waarde wordt gezocht. De generator-iterator output_colors levert een oneindig aantal kleuren die willekeurig uit de invoerlijst worden gekozen. Deze generator raakt nooit uitgeput.
Het is niet mogelijk om oneindige datastructuren zoals lijsten en tuples te maken. Met generators kan een programma wel oneindige iterables maken. Let op: als de generator-iterator rechtstreeks in een for-lus wordt gebruikt, zal de lus eeuwig blijven draaien.
Generators hebben meer geavanceerde toepassingsmogelijkheden in Python. In deze sectie verkennen we er enkele.
Generators kunnen ook extra gegevens accepteren die kunnen worden gebruikt tijdens de uitvoering van code. De regel met het trefwoord yield is een expressie die tot een waarde evalueert. Deze waarde kan binnen de generatorfunctie aan een variabele worden toegewezen. Laten we beginnen met een eenvoudig voorbeeld om dit concept te demonstreren:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
Het Python-trefwoord yield creëert een expressie die tot een waarde evalueert. De waarde van deze expressie binnen de generatorfunctie is echter niet hetzelfde object als dat door de generator wordt geleverd. Beschouw de eerste yield-expressie. De generator levert het geheel getal 1. Daarom toont print(next(output)) de waarde 1 bij de eerste aanroep en pauzeert de uitvoering van de generator.
Maar de yield-expressie in de generator evalueert tot een object dat aan de variabele value wordt toegewezen. In dit voorbeeld kent yield de waarde None toe aan value. Dit proces wordt herhaald bij de tweede yield in de generatorfunctie. Het doel van de derde next()-aanroep is om ervoor te zorgen dat alle code in de generatorfunctie wordt uitgevoerd.
Laten we de tweede en derde aanroep van next() vervangen door .send(), een methode van de generatorklasse:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
De generatorfunctie is ongewijzigd. De generator wordt gestart door next() aan te roepen, en de code loopt door totdat het eerste geheel getal, 1, wordt geleverd. In plaats van next() de tweede keer te gebruiken, roept het programma output.send() aan. Deze methode stuurt een object naar de generator. In dit voorbeeld is dat een string. De yield-expressie in de generatorfunctie evalueert tot deze string, die wordt toegewezen aan value. De generator kan de string dus binnen zijn code gebruiken.
De tweede aanroep van .send() stuurt een nieuw object naar de generator, dat wordt toegewezen aan dezelfde variabele value. Na de laatste print()-aanroep wordt een StopIteration opgegooid omdat er geen yield-expressies meer zijn.
Laten we nog een voorbeeld met .send() bekijken. De volgende generator toont het saldo op een rekening, maar het saldo kan worden aangepast:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
De generatorfunctie vereist een startsaldo bij aanroep. De waarde van balance kan veranderen terwijl de generator de code uitvoert. Elk object dat met .send() naar de generator wordt gestuurd, wordt toegewezen aan amount. Deze variabele is ofwel None als de generator een waarde levert zonder dat er een object naar is gestuurd, of bevat het object dat via .send() is doorgestuurd.
De generator-iterator current_balance start met een saldo van $100. De generator wordt gestart door next() aan te roepen, wat de code laat lopen tot de eerste waarde is geleverd.
Zodra de generator is gestart, kun je de uitvoering hervatten met .send() in plaats van next(). De generator telt de verzonden waarde bij het saldo op. Als er geen waarde wordt verzonden, bijvoorbeeld door next() opnieuw aan te roepen, levert de generator het ongewijzigde saldo.
Python-generators kunnen ook waarden rechtstreeks uit een andere generator of iterable leveren met de syntaxis yield from. Laten we een voorbeeld bekijken van een generatorfunctie die waarden uit een geneste lijst levert:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
De generatorfunctie accepteert een lijst, die geneste lijsten kan bevatten. De for-lus itereert over de items in de lijst. Elk item in de buitenste lijst is ofwel een waarde, in dit geval een geheel getal, of een andere lijst. Als het item geen lijst is, levert de generator het item.
Als het item wél een lijst is, roept de generator recursief de generatorfunctie flatten() aan met de binnenste lijst als argument. Dit creëert een andere generator-iterator, die de binnenste lijst als gegevensbron gebruikt. Als deze regel een yield-expressie zou gebruiken, zou de eerste generator de tweede generator zelf leveren. Door yield from te gebruiken, levert de eerste generator in plaats daarvan de waarden van de tweede generator.
yield vs. returnFunctiedefinities met return en yield lijken op elkaar, maar hun gedrag is anders. Laten we de belangrijkste verschillen samenvatten:
|
Gewone functie |
Generatorfunctie |
|
|
Trefwoord |
return (impliciet als het niet expliciet wordt gebruikt) |
yield |
|
Aanroep |
Voert code uit tot return wordt bereikt en retourneert dan de eindwaarde |
Maakt een generator-iterator |
|
Beëindiging |
Beëindigd door het return-statement |
Gepauzeerd door yield, later te hervatten |
|
Retourwaarde |
Eén object (kan een datastructuur zijn) |
Generator-iterator |
|
Yield-expressie |
Niet van toepassing (maakt een statement) |
Evalueert tot None of de waarde die met .send() is gestuurd |
|
Use-cases |
Ideaal om een eindresultaat te retourneren |
Ideaal om een datastroom te creëren, vooral voor grote of oneindige reeksen |
Het Python-trefwoord yield wordt in functies gebruikt om een generatorfunctie te definiëren. Wanneer ze worden aangeroepen, maken deze functies generator-iterators. Generators zijn een voorbeeld van lazy evaluation in Python, waarbij expressies pas worden geëvalueerd wanneer de waarde nodig is in plaats van bij het uitvoeren van de expressie. Daarom is de yield-expressie handig om een datastroom te creëren waarbij de waarden op aanvraag worden gegenereerd zonder dat ze in het geheugen hoeven te worden opgeslagen.
Efficiëntie is belangrijk bij grote datasets die veel bewerkingen vereisen. De generator-iterators van Python zijn een van de belangrijkste hulpmiddelen om grote hoeveelheden gegevens efficiënt te verwerken.
Wil je meer leren over Python? Bekijk dan deze Python Developer-carrièretrack.
Ik studeerde natuurkunde en wiskunde op bachelorniveau aan de Universiteit van Malta. Daarna verhuisde ik naar Londen en behaalde ik mijn PhD in natuurkunde aan Imperial College. Ik werkte aan nieuwe optische technieken om het menselijk netvlies in beeld te brengen. Nu richt ik me op schrijven over Python, communiceren over Python en lesgeven in Python.
Leer Python met deze cursussen!
Cursus
Cursus
Leerpad

blog
Adel Nehme
15 min