Generatoren in Python

Generatoren in Python

Dit artikel legt generatoren in Python uit.

YouTube Video

Generatoren in Python

Overzicht

Python generatoren zijn een soort iterator en een krachtig hulpmiddel om herhaalde bewerkingen efficiënt uit te voeren. Ze maken het mogelijk geheugenefficiënte code te schrijven wanneer je veel gegevens verwerkt.

Wat is een generator?

Een generator in Python is een speciale functie die één waarde tegelijk oplevert, gedefinieerd met het yield-trefwoord. Het kenmerk is dat de uitvoering wordt gepauzeerd terwijl de status behouden blijft en later kan worden hervat.

De basis van yield

yield is een trefwoord dat een waarde oplevert en tegelijkertijd de uitvoering van de functie pauzeert.

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • Wanneer aangeroepen, retourneert deze functie een generatorobject dat waarden één voor één oplevert.
  • Als je next() aanroept wanneer er geen volgende waarde is, treedt er een StopIteration-fout op.

next() en StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • Door de StopIteration-fout expliciet op deze manier af te handelen, kun je detecteren wanneer een generator klaar is.

send(value)

Het aanroepen van send(value) hervat de generator en stuurt value naar de positie van de yield-expressie. De verzonden waarde kan aan de kant van de generator worden ontvangen als de terugkeerwaarde van de yield-expressie. Bij de eerste aanroep kun je niets anders dan None verzenden met send(value), dus je moet next() of send(None) gebruiken.

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • Met send(10) wordt de yield van de generator een expressie die 10 retourneert en wordt 10 toegewezen aan x.

throw()

Het aanroepen van throw hervat de generator en werpt een uitzondering op de positie van de gepauzeerde yield. Je kunt de uitzondering binnen de generator afhandelen om de verwerking voort te zetten. Als de uitzondering niet wordt opgevangen, wordt deze naar buiten doorgegeven en stopt de generator.

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • In deze code wordt throw aangeroepen om een uitzondering in de generator te injecteren. Aan de zijde van de generator wordt de uitzondering afgehandeld en wordt recovered geretourneerd.

close()

Het aanroepen van close() beëindigt de generator. Binnen de generator kun je opruimacties uitvoeren met behulp van finally. Het aanroepen van next() of send() na het aanroepen van close() veroorzaakt een StopIteration-fout.

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • Deze code laat zien dat het aanroepen van close() de generator beëindigt en het opruimproces in finally activeert.

yield from

yield from is syntaxis die wordt gebruikt voor delegatie naar een subgenerator. Het is een eenvoudige manier om een andere generator binnen een generator aan te roepen en al zijn waarden door te geven aan de buitenste scope.

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • Deze code delegeert alle waarden van de subgenerator naar de buitengenerator met yield from en geeft daarna 3 terug.

Relatie met iterators

Generatoren implementeren intern __iter__() en __next__(), waardoor ze een soort iterator zijn. Daarom zijn ze volledig compatibel met iterabele bewerkingen, zoals for-lussen.

Integratie met for-lussen

In Python gebruikt een for-lus intern next() om automatisch waarden op te halen.

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

Met deze methode wordt de afhandeling van StopIteration ook automatisch gedaan.

Oneindige generatoren maken

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

Het is mogelijk om oneindige lussen te maken, maar wees voorzichtig bij het gebruik hiervan.

Generatorexpressies

Generatorexpressies, geschreven met haakjes, maken het mogelijk om generatoren te definiëren met een syntaxis vergelijkbaar met lijstcomprehensies.

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

In tegenstelling tot lijstcomprehensies laden ze niet alle elementen ineens in het geheugen, waardoor ze efficiënter zijn in geheugenverbruik.

Foutafhandeling in generatoren

Binnen een generator kunnen uitzonderingen optreden. In zulke gevallen gebruik je try-except, net als bij gewone Python-code.

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

In dit voorbeeld wordt correcte foutafhandeling uitgevoerd in het geval van deling door nul.

Stacktrace van een generator

Als er een uitzondering optreedt binnen de generator, wordt deze opgeworpen wanneer de generator wordt hervat.

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • Deze generator geeft eerst 1 terug. De fout die bij het hervatten wordt opgeworpen, wordt opgevangen en als een foutmelding weergegeven.

Voorbeelden van het gebruik van generatoren

Een bestand regel voor regel lezen (geschikt voor grote bestanden)

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • Deze functie leest een tekstbestand regel voor regel met een iterator, verwijdert witruimte van elke regel, en geeft het terug als een generator, zodat grote bestanden met een laag geheugengebruik verwerkt kunnen worden.

Generator voor de Fibonacci-reeks

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • Deze code gebruikt een generator om opeenvolgende Fibonacci-getallen kleiner dan de bovengrens te genereren en voert ze uit met een for-lus.

Toepassingen

Generators kunnen ook in de volgende scenario's worden gebruikt.

  • Sequentiële verwerking van grote CSV- of logbestanden
  • API-paginering
  • Verwerken van streaminggegevens (zoals Kafka, IoT-apparaten)

Samenvatting

Begrip Belangrijk punt
yield Pauzeert en geeft een waarde terug
Generatorfunctie Een functie die yield bevat en een iterator retourneert wanneer deze wordt aangeroepen
Voordelen Geheugenefficiënt en ideaal voor het verwerken van grote datasets
Generatorexpressie Staat een beknopte syntaxis toe zoals (x for x in iterable)

Door generatoren te gebruiken kun je grote datasets efficiënt verwerken, terwijl je geheugen bespaart en je code beknopt houdt.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video