3.1 Einführung in Generatoren
Generatoren sind Funktionen, die ein Iterator-Objekt zurückgeben. Diese Iteratoren generieren Werte, wenn sie angefordert werden, was es ermöglicht, potenziell große Datensätze zu verarbeiten, ohne sie vollständig in den Speicher zu laden.
Es gibt mehrere Möglichkeiten, Generatoren zu erstellen, unten werden die beliebtesten davon betrachtet.
Generatoren basierend auf Funktionen
Generatoren werden mit dem Schlüsselwort yield innerhalb einer Funktion erstellt. Wenn eine Funktion mit yield aufgerufen wird, gibt sie ein Generator-Objekt zurück, führt aber nicht sofort den Code innerhalb der Funktion aus. Stattdessen wird die Ausführung an der yield-Anweisung angehalten und bei jedem Aufruf der __next__()-Methode des Generator-Objekts fortgesetzt.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # Ausgabe: 1
print(next(counter)) # Ausgabe: 2
print(next(counter)) # Ausgabe: 3
print(next(counter)) # Ausgabe: 4
print(next(counter)) # Ausgabe: 5
Wenn eine Funktion das yield-Statement enthält, erstellt Python anstelle der traditionellen Ausführung der Funktion ein Generator-Objekt, das den Zustand der Ausführung der Funktion verwaltet.
Generator-Ausdrücke
Generator-Ausdrücke ähneln List Comprehensions, werden jedoch mit runden statt eckigen Klammern erstellt. Sie geben ebenfalls ein Generator-Objekt zurück.
squares = (x ** 2 for x in range(10))
print(next(squares)) # Ausgabe: 0
print(next(squares)) # Ausgabe: 1
print(next(squares)) # Ausgabe: 4
Welche Variante gefällt dir besser?
3.2 Vorteile von Generatoren
Effiziente Speichernutzung
Generatoren berechnen Werte on-the-fly, was es ermöglicht, große Datenmengen zu verarbeiten, ohne sie vollständig in den Speicher zu laden. Das macht Generatoren zur idealen Wahl für die Arbeit mit großen Datenmengen oder Datenströmen.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# Verarbeite Werte einzeln
print(value)
Lazy Evaluation
Generatoren führen lazy Evaluation durch, was bedeutet, dass sie Werte nur dann berechnen, wenn dies erforderlich ist. Dies hilft, unnötige Berechnungen zu vermeiden und die Leistung zu verbessern.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
Bequeme Syntax
Generatoren bieten eine bequeme Syntax zur Erstellung von Iteratoren, was das Schreiben und Lesen von Code vereinfacht.
3.3 Nutzung von Generatoren
Beispiele für die Nutzung von Generatoren in der Standardbibliothek
Viele Funktionen in der Python-Standardbibliothek nutzen Generatoren. Zum Beispiel gibt die Funktion range() ein Generator-Objekt zurück, das eine Sequenz von Zahlen generiert.
for i in range(10):
print(i)
Ja, die Welt wird nie mehr dieselbe sein.
Erstellung unendlicher Sequenzen
Generatoren ermöglichen die Erstellung unendlicher Sequenzen, die in verschiedenen Szenarien nützlich sein können, wie z.B. der Generierung unendlicher Datenströme.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
Verwendung von send() und close()
Generator-Objekte unterstützen die Methoden send() und close(), die es ermöglichen, Werte zurück in den Generator zu senden und dessen Ausführung zu beenden.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # Starte den Generator
e.send("Hallo, Welt!") # Ausgabe: Hallo, Welt!
e.close()
3.4 Generatoren in der Praxis
Generatoren und Ausnahmen
Generatoren können Ausnahmen behandeln, was sie zu einem mächtigen Werkzeug für das Schreiben von robusterem Code macht.
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("Generator geschlossen")
gen = controlled_execution()
print(next(gen)) # Ausgabe: Start
print(next(gen)) # Ausgabe: Working
gen.close() # Ausgabe: Generator geschlossen
Die Arbeit mit Ausnahmen werden wir in den nächsten Vorlesungen genauer betrachten, aber ich denke, es ist nützlich zu wissen, dass Generatoren gut damit umgehen.
Eingebettete Generatoren
Generatoren können ineinander verschachtelt sein, was es ermöglicht, komplexe iterierbare Strukturen zu erstellen.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# Ausgabe
0
1
2
A
B
C
Erklärung:
yield from: Diese Konstruktion wird verwendet, um einen Teil der Operationen an einen anderen Generator zu delegieren, was den Code vereinfacht und die Lesbarkeit verbessert.
Generatoren und Leistung
Die Verwendung von Generatoren kann die Leistung von Programmen erheblich verbessern, indem der Speicherverbrauch reduziert und die Iterationen effizienter ausgeführt werden.
Beispielvergleich von Listen und Generatoren
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# Nutzung einer Liste
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# Nutzung eines Generators
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # Berechnen der Summe für vergleichbare Ergebnisse
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"Liste:")
print(f" Zeit: {list_time:.2f} Sek")
print(f" Speicher: {list_memory:,} Bytes")
print(f"\nGenerator:")
print(f" Zeit: {gen_time:.2f} Sek")
print(f" Speicher: {gen_memory:,} Bytes")
Liste:
Zeit: 0.62 Sek
Speicher: 89,095,160 Bytes
Generator:
Zeit: 1.13 Sek
Speicher: 200 Bytes
GO TO FULL VERSION