36. Generatoren und Lazy Iteration
In Kapitel 35 haben wir gelernt, wie Iteration in Python über Iterables und Iteratoren funktioniert. Wir haben gesehen, dass Iteratoren Werte auf Anfrage jeweils einzeln zurückgeben, wodurch Python Sequenzen verarbeiten kann, ohne alles auf einmal in den Speicher zu laden. Jetzt werden wir Generatoren (generators) erkunden, Python’s eleganteste und praktischste Möglichkeit, Iteratoren zu erstellen.
Generatoren sind Funktionen, die ihre Ausführung anhalten und fortsetzen können und dabei Werte einzeln erzeugen, sobald sie angefordert werden, statt alle Werte im Voraus zu berechnen und im Speicher zu speichern. Dieser Ansatz—genannt Lazy Evaluation (lazy evaluation)—bedeutet, dass Werte nur dann erzeugt werden, wenn sie benötigt werden, was ihn zu einer der mächtigsten Python-Funktionen für speichereffizienten Code macht.
36.1) Was Generatoren sind und warum sie nützlich sind
36.1.1) Das Problem beim Erstellen großer Listen
Beginnen wir damit, das Problem zu verstehen, das Generatoren lösen. Angenommen, Sie müssen eine Sequenz von einer Million Zahlen verarbeiten. Hier ist der traditionelle Ansatz mit einer Liste:
# Eine Liste mit einer Million Quadraten erstellen
def get_squares_list(n):
"""Return a list of squares from 0 to n-1."""
squares = []
for i in range(n):
squares.append(i * i)
return squares
# Dies erstellt eine Liste mit 1.000.000 Zahlen im Speicher
numbers = get_squares_list(1_000_000)
print(f"First five squares: {numbers[:5]}") # Output: First five squares: [0, 1, 4, 9, 16]Dieser Ansatz hat ein erhebliches Problem: Er erstellt und speichert alle eine Million Zahlen gleichzeitig im Speicher, selbst wenn Sie sie nur einzeln verarbeiten müssen. Bei größeren Datensätzen oder komplexeren Berechnungen kann das enorme Mengen an Speicher verbrauchen oder sogar Ihr Programm zum Absturz bringen.
36.1.2) Generatoren einführen: Werte bei Bedarf berechnen
Ein Generator (generator) ist eine besondere Art von Funktion, die Werte einzeln erzeugt, und zwar nur auf Anfrage. Statt eine vollständige Liste aufzubauen und zurückzugeben, berechnet ein Generator jeden Wert bei Bedarf und „merkt“ sich zwischen Aufrufen, wo er aufgehört hat.
Hier ist dieselbe Funktionalität als Generator implementiert:
# Einen Generator mit Quadraten erstellen
def get_squares_generator(n):
"""Generate squares from 0 to n-1, one at a time."""
for i in range(n):
yield i * i # yield pausiert die Funktion und gibt einen Wert zurück
# Dies erstellt ein Generator-Objekt, keine Liste
squares_gen = get_squares_generator(1_000_000)
print(squares_gen) # Output: <generator object get_squares_generator at 0x...>
# Werte einzeln abrufen
print(next(squares_gen)) # Output: 0
print(next(squares_gen)) # Output: 1
print(next(squares_gen)) # Output: 4Der Generator berechnet nicht alle eine Million Quadrate im Voraus. Stattdessen berechnet er jedes Quadrat erst dann, wenn Sie next() darauf aufrufen. Zwischen den Aufrufen „pausiert“ der Generator und merkt sich seinen Zustand (den aktuellen Wert von i).
36.1.3) Speichereffizienz: Der entscheidende Vorteil
Der Speicherunterschied zwischen Listen und Generatoren wird bei großen Datensätzen dramatisch. Vergleichen wir:
import sys
# Listen-Ansatz: speichert alle Werte
def squares_list(n):
return [i * i for i in range(n)]
# Generator-Ansatz: berechnet Werte bei Bedarf
def squares_generator(n):
for i in range(n):
yield i * i
# Speichernutzung für 100.000 Zahlen vergleichen
list_result = squares_list(100_000)
gen_result = squares_generator(100_000)
print(f"List size in memory: {sys.getsizeof(list_result):,} bytes")
# Output: List size in memory: 800,984 bytes (actual size may vary)
print(f"Generator size in memory: {sys.getsizeof(gen_result)} bytes")
# Output: Generator size in memory: 200 bytes (actual size may vary)Die Liste verbraucht über 800 KB Speicher, während der Generator nur 200 Bytes nutzt—unabhängig davon, wie viele Werte er letztlich produzieren wird. Der Generator speichert nur den Zustand der Funktion (den aktuellen Wert von i und die Stelle, an der fortgesetzt wird), nicht die eigentliche Folge der Werte.
36.1.4) Wann Generatoren nützlich sind
Generatoren sind in mehreren häufigen Szenarien besonders stark:
Große Dateien verarbeiten:
def read_large_file(filename):
"""Generate lines from a file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
# Eine riesige Logdatei verarbeiten, ohne alles in den Speicher zu laden
for line in read_large_file('huge_log.txt'):
if 'ERROR' in line:
print(line)Unendliche Sequenzen:
def fibonacci():
"""Generate Fibonacci numbers indefinitely."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Fibonacci-Zahlen für immer erzeugen (oder bis Sie aufhören zu fragen)
fib = fibonacci()
print([next(fib) for _ in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]36.1.5) Generatoren sind Iteratoren
Wie wir in Kapitel 35 gelernt haben, sind Generatoren tatsächlich eine spezielle Art von Iterator. Sie implementieren automatisch das Iterator-Protokoll (__iter__() und __next__()), weshalb sie nahtlos mit for-Schleifen funktionieren:
def countdown(n):
"""Generate countdown from n to 1."""
while n > 0:
yield n
n -= 1
# Generatoren funktionieren direkt in for-Schleifen
for num in countdown(5):
print(num)
# Output:
# 5
# 4
# 3
# 2
# 1Wenn Sie einen Generator in einer for-Schleife verwenden, ruft Python automatisch wiederholt next() darauf auf, bis der Generator erschöpft ist (wirft StopIteration).
36.2) Generatorfunktionen mit yield erstellen
36.2.1) Die yield-Anweisung: Pausieren und Fortsetzen
Die yield-Anweisung ist das, was eine Funktion zu einem Generator macht. Wenn Python auf yield trifft, passiert etwas Besonderes: Statt einen Wert zurückzugeben und die Funktion zu beenden, pausiert es die Funktion und gibt den Wert zurück. Beim nächsten Aufruf von next() auf dem Generator wird die Ausführung direkt nach der yield-Anweisung fortgesetzt.
Hier ist ein einfaches Beispiel, das dieses Pause-und-Fortsetzen-Verhalten demonstriert:
def simple_generator():
"""Demonstrate how yield pauses execution."""
print("Starting generator")
yield 1
print("Resuming after first yield")
yield 2
print("Resuming after second yield")
yield 3
print("Generator finished")
gen = simple_generator()
print("Created generator")
# Output:
# Created generator
print(f"First value: {next(gen)}")
# Output:
# Starting generator
# First value: 1
print(f"Second value: {next(gen)}")
# Output:
# Resuming after first yield
# Second value: 2
print(f"Third value: {next(gen)}")
# Output:
# Resuming after second yield
# Third value: 3
try:
next(gen)
except StopIteration:
print("Generator exhausted - no more values")
# Output:
# Generator finished
# Generator exhausted - no more valuesBeachten Sie, wie die Ausführung der Funktion mit den Aufrufen von next() verschachtelt ist. Jedes yield pausiert die Funktion, und jedes next() setzt sie dort fort, wo sie aufgehört hat.
36.2.2) Generatorzustand: Lokale Variablen merken
Generatoren merken sich alle ihre lokalen Variablen zwischen den yield-Aufrufen. Das macht sie nützlich, um Zustand über mehrere Aufrufe hinweg zu behalten:
def counter(start=0):
"""Generate sequential numbers starting from start."""
current = start
while True:
yield current
current += 1
# Der Generator merkt sich 'current' zwischen yield-Aufrufen
count = counter(10)
print(next(count)) # Output: 10
print(next(count)) # Output: 11
print(next(count)) # Output: 12
# Jeder Generator hat seinen eigenen unabhängigen Zustand
count1 = counter(0)
count2 = counter(100)
print(next(count1)) # Output: 0
print(next(count2)) # Output: 100
print(next(count1)) # Output: 1
print(next(count2)) # Output: 101Die Variable current bleibt jedes Mal erhalten, wenn der Generator bei einem yield pausiert und beim nächsten next()-Aufruf fortgesetzt wird. Dadurch kann der Generator vom letzten Wert aus weiterzählen. Jede Generator-Instanz verwaltet ihren eigenen unabhängigen Zustand.
36.2.3) Yield in Schleifen: Das häufigste Muster
Die häufigste Verwendung von Generatoren ist das Ausgeben von Werten innerhalb einer Schleife. Dieses Muster erzeugt eine Folge von Werten:
def even_numbers(start, end):
"""Generate even numbers in the given range."""
current = start if start % 2 == 0 else start + 1
while current <= end:
yield current
current += 2
# Den Generator verwenden
evens = even_numbers(1, 20)
print(list(evens))
# Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]Jede Iteration der Schleife liefert einen Wert, und dann geht es zur nächsten Iteration weiter, wenn next() erneut aufgerufen wird.
36.2.4) Mehrere yield-Anweisungen
Ein Generator kann mehrere yield-Anweisungen an unterschiedlichen Stellen in seinem Code haben. Die Ausführung läuft der Reihe nach durch sie hindurch:
def process_data(data):
"""Generate processed data with status messages."""
yield "Starting processing..."
cleaned = [item.strip().lower() for item in data]
yield f"Cleaned {len(cleaned)} items"
unique = list(set(cleaned))
yield f"Found {len(unique)} unique items"
for item in sorted(unique):
yield item
# Einige Daten verarbeiten
data = [" Apple ", "Banana", "apple", "Cherry", "BANANA"]
processor = process_data(data)
for result in processor:
print(result)
# Output:
# Starting processing...
# Cleaned 5 items
# Found 3 unique items
# apple
# banana
# cherryDieses Muster ist nützlich für Generatoren, die Vorbereitungsarbeit leisten müssen, Statusinformationen liefern und anschließend die eigentlichen Daten ausgeben.
36.3) Generatorausdrücke vs. List Comprehensions
36.3.1) Generatorausdrücke einführen
In Kapitel 34 haben wir List Comprehensions kennengelernt—eine knappe Möglichkeit, Listen zu erstellen. Generatorausdrücke (generator expressions) verwenden nahezu identische Syntax, erzeugen jedoch Generatoren statt Listen.
Ein Generatorausdruck ist im Grunde eine kompakte Möglichkeit, eine einfache Generatorfunktion zu schreiben. Vergleichen Sie diese zwei äquivalenten Ansätze:
# Generatorfunktion
def squares_function(n):
for x in range(n):
yield x * x
# Generatorausdruck - macht dasselbe
squares_expression = (x * x for x in range(10))
# Beide erzeugen Generator-Objekte
gen1 = squares_function(10)
gen2 = squares_expression
print(type(gen1)) # Output: <class 'generator'>
print(type(gen2)) # Output: <class 'generator'>
# Beide liefern dieselben Werte
print(list(squares_function(10))) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_expression)) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Die Syntax ist fast identisch zu List Comprehensions. Die Unterschiede sind: Verwenden Sie runde Klammern () statt eckiger Klammern [], und während List Comprehensions Listen erstellen, erzeugen Generatorausdrücke Generatoren:
# List Comprehension - erstellt die gesamte Liste im Speicher
squares_list = [x * x for x in range(10)]
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Generatorausdruck - erstellt ein Generatorobjekt
squares_gen = (x * x for x in range(10))
print(squares_gen)
# Output: <generator object <genexpr> at 0x...>
# In eine Liste umwandeln, um Werte zu sehen
print(list(squares_gen))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Generatorausdrücke bieten dieselbe kompakte Syntax wie List Comprehensions, aber mit der Speichereffizienz von Generatoren.
36.3.2) Speichervergleich: Wann es wichtig ist
Für kleine Sequenzen ist der Speicherunterschied zwischen List Comprehensions und Generatorausdrücken vernachlässigbar. Bei großen Sequenzen wird er jedoch erheblich:
import sys
# Kleine Sequenz - minimaler Unterschied
small_list = [x for x in range(100)]
small_gen = (x for x in range(100))
print(f"Small list: {sys.getsizeof(small_list)} bytes")
# Output: Small list: 920 bytes (actual size may vary)
print(f"Small generator: {sys.getsizeof(small_gen)} bytes")
# Output: Small generator: 192 bytes (actual size may vary)
# Große Sequenz - riesiger Unterschied
large_list = [x for x in range(1_000_000)]
large_gen = (x for x in range(1_000_000))
print(f"Large list: {sys.getsizeof(large_list):,} bytes")
# Output: Large list: 8,448,728 bytes (actual size may vary)
print(f"Large generator: {sys.getsizeof(large_gen)} bytes")
# Output: Large generator: 192 bytes (actual size may vary)Die Größe des Generators bleibt konstant, unabhängig davon, wie viele Werte er produzieren wird—er speichert nur den Ausdruck und den aktuellen Zustand. Die Liste hingegen muss alle Werte im Speicher halten, weshalb ihre Größe proportional zur Anzahl der Elemente wächst.
36.3.3) Generatorausdrücke in Funktionsaufrufen
Generatorausdrücke sind besonders elegant, wenn sie direkt an Funktionen übergeben werden, die Iterables konsumieren. Sie können die zusätzlichen Klammern weglassen, wenn ein Generatorausdruck das einzige Argument ist:
# Summe der Quadrate berechnen, ohne eine Liste zu erstellen
total = sum(x * x for x in range(100)) # Hinweis: keine zusätzlichen Klammern nötig
print(total)
# Output: 328350
# Maximum transformierter Werte finden
numbers = [1, 2, 3, 4, 5]
max_square = max(x * x for x in numbers)
print(max_square)
# Output: 25
# Prüfen, ob irgendein Wert eine Bedingung erfüllt
data = [10, 15, 20, 25, 30]
has_large = any(x > 100 for x in data)
print(has_large)
# Output: FalseDieses Muster ist sowohl speichereffizient als auch gut lesbar. Funktionen wie sum(), max(), min(), any() und all() verarbeiten den Generator Wert für Wert und erstellen niemals eine Zwischenliste.
36.3.4) Filtern mit Generatorausdrücken
Generatorausdrücke unterstützen dieselbe bedingte Logik wie List Comprehensions:
# Gerade Zahlen filtern
numbers = range(20)
evens = (x for x in numbers if x % 2 == 0)
print(list(evens))
# Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Transformieren und filtern
words = ["hello", "world", "python", "programming"]
long_upper = (word.upper() for word in words if len(word) > 5)
print(list(long_upper))
# Output: ['PYTHON', 'PROGRAMMING']36.3.5) Wann Generatorausdrücke nicht ausreichen
Generatorausdrücke sind kurz und elegant, aber sie haben Einschränkungen. Verwenden Sie Generatorfunktionen, wenn Sie Folgendes brauchen:
Komplexe Logik:
# Zu komplex für einen Generatorausdruck
def process_log_lines(filename):
"""Process log file with complex logic."""
with open(filename, 'r') as file:
for line in file:
line = line.strip()
if not line or line.startswith('#'):
continue # Leere Zeilen und Kommentare überspringen
parts = line.split('|')
if len(parts) >= 3:
timestamp, level, message = parts[0], parts[1], parts[2]
if level in ('ERROR', 'CRITICAL'):
yield {
'timestamp': timestamp,
'level': level,
'message': message
}Mehrere Yields oder Zustand:
# Ein Generatorausdruck kann keinen Zustand über Iterationen hinweg halten
def running_total(numbers):
"""Generate running total of numbers."""
total = 0
for num in numbers:
total += num
yield total
numbers = [1, 2, 3, 4, 5]
print(list(running_total(numbers)))
# Output: [1, 3, 6, 10, 15]Fehlerbehandlung:
# Ein Generatorausdruck kann keine Exceptions behandeln
def safe_divide(numbers, divisor):
"""Generate division results, handling errors."""
for num in numbers:
try:
yield num / divisor
except ZeroDivisionError:
yield float('inf')36.4) Wann man Generatoren statt Listen verwendet
36.4.1) Große Datensätze: Der wichtigste Anwendungsfall
Der überzeugendste Grund, Generatoren zu verwenden, ist die Arbeit mit großen Datenmengen. Wenn Sie Millionen von Datensätzen verarbeiten, können Generatoren den Unterschied zwischen einem Programm ausmachen, das reibungslos läuft, und einem, das abstürzt.
Schlechter Ansatz – Gesamte Datei in den Speicher laden:
# TUN SIE DAS NICHT bei großen Dateien
def count_errors_bad(filename):
"""Load entire file into memory - will crash with large files."""
with open(filename, 'r') as file:
lines = file.readlines() # Lädt die GESAMTE Datei in den Speicher
error_count = 0
for line in lines:
if 'ERROR' in line:
error_count += 1
return error_count
# Wenn die Datei 10 GB groß ist, versucht dies, 10 GB in den Speicher zu laden!Guter Ansatz – Einen Generator verwenden:
def read_log_lines(filename):
"""Generate lines from a log file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
def count_errors_good(filename):
"""Count errors without loading entire file into memory."""
error_count = 0
for line in read_log_lines(filename):
if 'ERROR' in line:
error_count += 1
return error_count
# Das funktioniert effizient sogar mit gigabytegroßen Logdateien,
# weil immer nur eine Zeile gleichzeitig im Speicher gehalten wird
count = count_errors_good('huge_application.log')
print(f"Found {count} errors")Der Generatoransatz verarbeitet jeweils eine Zeile, daher bleibt der Speicherverbrauch konstant, unabhängig von der Dateigröße. Eine 10-GB-Datei nutzt genauso viel Speicher wie eine 10-KB-Datei.
36.4.2) Unendliche oder Sequenzen unbekannter Länge
Generatoren sind perfekt für Sequenzen, bei denen Sie die Länge nicht im Voraus kennen oder bei denen die Sequenz konzeptionell unendlich ist:
def user_input_stream():
"""Generate user inputs until they type 'quit'."""
while True:
user_input = input("Enter a number (or 'quit'): ")
if user_input.lower() == 'quit':
break
try:
yield int(user_input)
except ValueError:
print("Invalid number, try again")
# Benutzereingaben verarbeiten, während sie eintreffen
total = 0
count = 0
for number in user_input_stream():
total += number
count += 1
print(f"Running average: {total / count:.2f}")Sie können keine Liste unbekannter Länge erstellen, aber ein Generator kann damit natürlich umgehen.
36.4.3) Verkettete Transformationen: Datenpipelines aufbauen
Wenn Sie mehrere Transformationen auf Daten anwenden müssen, erlauben Generatoren das Verketten von Operationen, ohne Zwischenlisten zu erstellen:
# Zahlen durch mehrere Stufen transformieren
def generate_numbers(n):
"""Generate numbers from 1 to n."""
for i in range(1, n + 1):
yield i
def square_numbers(numbers):
"""Generate squares of input numbers."""
for num in numbers:
yield num * num
def keep_even(numbers):
"""Generate only even numbers."""
for num in numbers:
if num % 2 == 0:
yield num
# Generatoren verketten - keine Zwischenlisten erstellt
numbers = generate_numbers(10)
squared = square_numbers(numbers)
even_squares = keep_even(squared)
# Ergebnisse verarbeiten
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]Jede Stufe verarbeitet jeweils einen Wert und reicht ihn an die nächste Stufe weiter. Das ist speichereffizient und ermöglicht es Ihnen, Datensätze zu verarbeiten, die größer sind als der verfügbare RAM.
Ohne Generatoren würden Sie Zwischenlisten benötigen:
# Ansatz ohne Generatoren - erstellt Zwischenlisten
numbers = list(range(1, 11)) # [1, 2, 3, ..., 10]
squared = [n * n for n in numbers] # [1, 4, 9, ..., 100]
even_squares = [n for n in squared if n % 2 == 0] # [4, 16, 36, 64, 100]
# Mit Generatoren - keine Zwischenlisten
numbers = (i for i in range(1, 11))
squared = (n * n for n in numbers)
even_squares = (n for n in squared if n % 2 == 0)
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]Für eine Pipeline mit drei Stufen, die eine Million Elemente verarbeitet, würde der Listenansatz drei Listen mit jeweils einer Million Elementen erzeugen. Der Generatoransatz hält jeweils nur einen Wert im Speicher.
36.4.4) Wann Listen besser sind als Generatoren
Trotz ihrer Vorteile sind Generatoren nicht immer die richtige Wahl. Verwenden Sie Listen, wenn Sie Folgendes brauchen:
Mehrfaches Iterieren:
# Liste - kann mehrfach iteriert werden
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
print(max(numbers)) # Output: 5 (works fine)
# Generator - kann nur einmal iteriert werden
numbers_gen = (x for x in range(1, 6))
print(sum(numbers_gen)) # Output: 15
print(max(numbers_gen)) # Output: ValueError: max() iterable argument is emptyWenn Sie dieselben Daten mehrfach verarbeiten müssen, verwenden Sie eine Liste.
Zufälliger Zugriff:
# Zugriff auf Elemente per Index nötig - verwenden Sie eine Liste
students = ['Alice', 'Bob', 'Charlie', 'Diana']
print(students[2]) # Output: Charlie
# Generatoren unterstützen kein Indexing
students_gen = (name for name in students)
# students_gen[2] # ERROR: 'generator' object is not subscriptableLängeninformation:
# Länge muss bekannt sein - verwenden Sie eine Liste
data = [1, 2, 3, 4, 5]
print(f"Processing {len(data)} items")
# Generatoren haben keine Länge
data_gen = (x for x in data)
# len(data_gen) # ERROR: object of type 'generator' has no len()Kleine Datensätze:
# Für kleine Datensätze sind Listen in Ordnung und bequemer
small_data = [x * 2 for x in range(10)]
# Die Speichereinsparung eines Generators ist hier nicht signifikant,
# und die Liste ist flexibler36.4.5) Praktischer Entscheidungsleitfaden
Hier ist ein praktischer Leitfaden, um zwischen Generatoren und Listen zu wählen:
Verwenden Sie Generatoren, wenn:
- Sie große Dateien oder Datensätze verarbeiten
- Sie mit Datenströmen oder Benutzereingaben arbeiten
- Sie Datenverarbeitungspipelines aufbauen
- Speichereffizienz wichtig ist
- Sie nur einmal iterieren müssen
- Die Sequenz unendlich oder sehr lang ist
Verwenden Sie Listen, wenn:
- Der Datensatz klein ist (typischerweise < 10.000 Elemente)
- Sie mehrfach iterieren müssen
- Sie zufälligen Zugriff per Index benötigen
- Sie die Länge kennen müssen
- Sie die Daten an Code übergeben müssen, der eine Liste erwartet
36.4.6) Zwischen Generatoren und Listen konvertieren
Sie können bei Bedarf leicht zwischen Generatoren und Listen konvertieren:
# Generator zu Liste
numbers_gen = (x * 2 for x in range(5))
numbers_list = list(numbers_gen)
print(numbers_list)
# Output: [0, 2, 4, 6, 8]
# Liste zu Generator (mit Generatorausdruck)
numbers_list = [1, 2, 3, 4, 5]
numbers_gen = (x for x in numbers_list)Diese Flexibilität bedeutet, dass Sie mit einem Generator für Effizienz beginnen und nur dann in eine Liste umwandeln können, wenn Sie listen-spezifische Funktionen brauchen:
# Mit einem Generator für Speichereffizienz beginnen
numbers = (x for x in range(1, 1001))
filtered = (x for x in numbers if x % 7 == 0)
# In eine Liste umwandeln, wenn Sie mehrfach iterieren müssen
multiples_of_seven = list(filtered)
# Jetzt können Sie Listenfunktionen verwenden
print(f"Count: {len(multiples_of_seven)}")
# Output: Count: 142
print(f"First: {multiples_of_seven[0]}")
# Output: First: 7
print(f"Last: {multiples_of_seven[-1]}")
# Output: Last: 994
# Kann mehrfach iterieren
total = sum(multiples_of_seven)
average = total / len(multiples_of_seven)
print(f"Average: {average:.1f}")
# Output: Average: 500.5Generatoren sind eine der elegantesten Python-Funktionen, um speichereffizienten Code zu schreiben. Sie erlauben es Ihnen, große Datensätze zu verarbeiten, Datenpipelines aufzubauen und mit unendlichen Sequenzen zu arbeiten—und dabei Ihren Code sauber und gut lesbar zu halten. Mit zunehmender Erfahrung entwickeln Sie ein Gefühl dafür, wann Generatoren das richtige Werkzeug für die Aufgabe sind.