Python-Generatoren: Vollständiger Leitfaden zu yield, Generator-Expressions und Lazy Evaluation
Updated on
Das Verarbeiten einer 10GB-Logdatei oder das Streamen von Millionen Datenbank-Records kann deine Python-Anwendung in die Knie zwingen. Der traditionelle Ansatz, alle Daten auf einmal in den Speicher zu laden, führt zu Performance-Engpässen, Memory Errors und frustrierten Nutzer:innen. Genau hier werden Python-Generatoren essenziell: Sie ermöglichen es dir, riesige Datensätze mit minimalem Speicherbedarf zu verarbeiten, indem Werte bei Bedarf (on-demand) erzeugt werden, statt alles vorab zu speichern.
Was sind Python-Generatoren und warum sie wichtig sind
Generatoren sind spezielle Funktionen, die im Verlauf der Zeit eine Sequenz von Werten erzeugen, statt alles auf einmal zu berechnen und zurückzugeben. Im Gegensatz zu normalen Funktionen, die mit return ein einzelnes Ergebnis zurückliefern, verwenden Generatoren das Keyword yield, um eine Reihe von Werten zu produzieren, die Ausführung zwischen den einzelnen Werten zu pausieren und erst dann fortzusetzen, wenn der nächste Wert angefordert wird.
Der grundlegende Vorteil von Generatoren ist Lazy Evaluation – Werte werden nur dann erzeugt, wenn sie tatsächlich benötigt werden. Das bringt zwei entscheidende Vorteile:
- Speichereffizienz: Generatoren speichern nicht die gesamte Sequenz im Speicher. Ein Generator, der eine Milliarde Zahlen produziert, verbraucht denselben Speicher wie einer, der zehn Zahlen produziert.
- Performance: Die Verarbeitung kann sofort mit dem ersten erzeugten Wert starten, ohne darauf zu warten, dass der gesamte Datensatz vorbereitet ist.
Hier ist ein einfacher Vergleich, der den Unterschied verdeutlicht:
# Traditional approach - loads entire list into memory
def get_squares_list(n):
result = []
for i in range(n):
result.append(i * i)
return result
# Generator approach - produces values one at a time
def get_squares_generator(n):
for i in range(n):
yield i * i
# Memory impact comparison
import sys
# List approach
squares_list = get_squares_list(1000000)
print(f"List memory: {sys.getsizeof(squares_list):,} bytes") # ~8,000,000 bytes
# Generator approach
squares_gen = get_squares_generator(1000000)
print(f"Generator memory: {sys.getsizeof(squares_gen):,} bytes") # ~112 bytesDer Speicherunterschied ist enorm – der Generator nutzt in diesem Beispiel 99,999% weniger Speicher als die Liste. Mit größeren Datensätzen wird dieser Effekt noch drastischer.
Das Keyword yield: Das Herz von Generator-Funktionen
Das Keyword yield ist es, das eine normale Funktion in eine Generator-Funktion verwandelt. Sobald Python auf yield trifft, weiß es, dass es ein Generator-Objekt zurückgeben soll, statt die Funktion sofort auszuführen.
def countdown(n):
print(f"Starting countdown from {n}")
while n > 0:
yield n
n -= 1
print("Countdown complete!")
# Creating the generator doesn't execute the function
gen = countdown(3)
print(type(gen)) # <class 'generator'>
# Values are produced on-demand
print(next(gen)) # Starting countdown from 3 -> 3
print(next(gen)) # 2
print(next(gen)) # 1
# next(gen) # Countdown complete! -> Raises StopIterationWichtige Verhaltensweisen, die du verstehen solltest:
- Die Ausführung pausiert bei jedem
yield-Statement und wird beim nächsten Aufruf exakt an dieser Stelle fortgesetzt - Lokale Variablen behalten ihren Zustand zwischen
yield-Aufrufen - Die StopIteration-Exception wird ausgelöst, wenn die Generator-Funktion zurückkehrt (keine Werte mehr hat)
Mehrere yield-Statements können in einem einzigen Generator vorkommen:
def data_pipeline():
# Phase 1: Loading
yield "Loading data..."
# Phase 2: Processing
yield "Processing records..."
# Phase 3: Validation
yield "Validating results..."
# Phase 4: Complete
yield "Pipeline complete!"
for status in data_pipeline():
print(status)Generator-Protokoll: iter() und next() verstehen
Generatoren implementieren das Iterator-Protokoll über zwei spezielle Methoden:
__iter__(): Gibt das Iterator-Objekt selbst zurück (den Generator)__next__(): Liefert den nächsten Wert aus dem Generator
Damit sind Generatoren perfekt für for-Loops und andere Iterations-Kontexte. Wenn du dieses Protokoll verstehst, wird klarer, wie Generatoren intern funktionieren:
def simple_gen():
yield 1
yield 2
yield 3
gen = simple_gen()
# These are equivalent
print(gen.__next__()) # 1
print(next(gen)) # 2
# for loops call __next__() automatically until StopIteration
for value in simple_gen():
print(value) # 1, 2, 3Du kannst das Iterator-Protokoll auch manuell implementieren, um generatorähnliches Verhalten zu erzeugen:
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# Behaves like a generator
for num in CountDown(3):
print(num) # 3, 2, 1Allerdings sind Generator-Funktionen deutlich kompakter und besser lesbar als manuelle Iterator-Klassen.
Generator-Expressions vs. List Comprehensions
Generator-Expressions bieten eine kompakte Syntax, um Generatoren zu erstellen – ähnlich wie List Comprehensions, aber mit Klammern statt eckigen Klammern:
# List comprehension - creates entire list in memory
squares_list = [x * x for x in range(10)]
print(type(squares_list)) # <class 'list'>
print(squares_list) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Generator expression - creates generator object
squares_gen = (x * x for x in range(10))
print(type(squares_gen)) # <class 'generator'>
print(squares_gen) # <generator object at 0x...>
# Consume the generator
print(list(squares_gen)) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Syntax-Vergleich:
| Feature | List Comprehension | Generator Expression |
|---|---|---|
| Syntax | [expr for item in iterable] | (expr for item in iterable) |
| Returns | List object | Generator object |
| Memory | Stores all values | Generates on-demand |
| Speed | Faster for small datasets | Faster for large datasets |
| Reusable | Yes (can iterate multiple times) | No (exhausted after one iteration) |
Praktisches Beispiel, das den Speicherunterschied zeigt:
import sys
# List comprehension for 1 million numbers
list_comp = [x for x in range(1000000)]
print(f"List comprehension: {sys.getsizeof(list_comp):,} bytes")
# Generator expression for the same range
gen_exp = (x for x in range(1000000))
print(f"Generator expression: {sys.getsizeof(gen_exp):,} bytes")
# Output:
# List comprehension: 8,000,056 bytes
# Generator expression: 112 bytesGenerator-Expressions sind ideal, wenn du Werte nur einmal durchlaufen musst und den Speicherverbrauch minimieren willst.
yield from: Delegation an Sub-Generatoren
Das Statement yield from vereinfacht die Delegation an Sub-Generatoren oder andere Iterables. Statt manuell zu loopen und jeden Wert zu yielden, erledigt yield from das automatisch:
# Without yield from
def get_numbers_manual():
for i in range(3):
yield i
for i in range(10, 13):
yield i
# With yield from
def get_numbers_delegated():
yield from range(3)
yield from range(10, 13)
print(list(get_numbers_manual())) # [0, 1, 2, 10, 11, 12]
print(list(get_numbers_delegated())) # [0, 1, 2, 10, 11, 12]Das ist besonders nützlich, um verschachtelte Strukturen zu „flatten“:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item) # Recursive delegation
else:
yield item
nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(list(flatten(nested))) # [1, 2, 3, 4, 5, 6, 7, 8, 9]yield from behandelt außerdem Exceptions und Return Values von Sub-Generatoren korrekt – wichtig für komplexe Generator-Pipelines.
Fortgeschritten: send() und throw() Methoden
Generatoren können mehr als nur Werte liefern – sie können über send() und throw() auch Werte empfangen und Exceptions verarbeiten. Das ermöglicht Coroutine-ähnliche bidirektionale Kommunikation.
send() verwenden, um Werte in Generatoren zu schicken
def running_average():
total = 0
count = 0
average = None
while True:
value = yield average # Yield current average, receive new value
total += value
count += 1
average = total / count
# Create generator
avg = running_average()
next(avg) # Prime the generator (advance to first yield)
# Send values and receive running averages
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
print(avg.send(40)) # 25.0Die Methode send() schickt einen Wert in den Generator (dieser wird zum Ergebnis des yield-Ausdrucks) und setzt die Ausführung bis zum nächsten yield fort.
throw() verwenden, um Exceptions einzuschleusen
def error_handling_gen():
try:
while True:
value = yield
print(f"Received: {value}")
except ValueError as e:
print(f"Caught ValueError: {e}")
yield "Recovered from error"
except GeneratorExit:
print("Generator is closing")
gen = error_handling_gen()
next(gen) # Prime the generator
gen.send(10) # Received: 10
gen.send(20) # Received: 20
result = gen.throw(ValueError, "Invalid value") # Caught ValueError: Invalid value
print(result) # Recovered from error
gen.close() # Generator is closingDiese fortgeschrittenen Features sind besonders hilfreich für State Machines, Coroutines und komplexe asynchrone Muster.
Unendliche Generatoren: Endlose Sequenzen
Generatoren eignen sich hervorragend, um unendliche Sequenzen zu produzieren, da sie die gesamte Sequenz nie im Speicher materialisieren müssen:
# Infinite counter
def count_from(start=0, step=1):
current = start
while True:
yield current
current += step
# Fibonacci sequence
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Cycling through a sequence
def cycle(iterable):
saved = []
for item in iterable:
yield item
saved.append(item)
while saved:
for item in saved:
yield item
# Usage examples
counter = count_from(10, 2)
for _ in range(5):
print(next(counter)) # 10, 12, 14, 16, 18
fib = fibonacci()
print([next(fib) for _ in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
colors = cycle(['red', 'green', 'blue'])
print([next(colors) for _ in range(8)]) # ['red', 'green', 'blue', 'red', 'green', 'blue', 'red', 'green']Unendliche Generatoren sind besonders nützlich für Event-Streams, kontinuierliches Monitoring und zustandsbehaftete Iterationsmuster.
Generatoren verketten: Datenverarbeitungs-Pipelines bauen
Eines der mächtigsten Patterns mit Generatoren ist, sie zu verketten, um effiziente Datenverarbeitungs-Pipelines zu bauen. Jede Stufe verarbeitet Daten lazy und reicht Ergebnisse an die nächste weiter – ohne Zwischenresultate zu speichern:
# Stage 1: Read lines from a file (generator)
def read_log_file(filename):
with open(filename, 'r') as f:
for line in f:
yield line.strip()
# Stage 2: Filter lines containing 'ERROR'
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
# Stage 3: Extract timestamp and message
def parse_error_lines(lines):
for line in lines:
parts = line.split(' - ')
if len(parts) >= 2:
yield {'timestamp': parts[0], 'message': parts[1]}
# Stage 4: Count errors by hour
def group_by_hour(errors):
from collections import defaultdict
hourly_counts = defaultdict(int)
for error in errors:
hour = error['timestamp'][:13] # Extract hour portion
hourly_counts[hour] += 1
return hourly_counts
# Build pipeline
log_lines = read_log_file('app.log')
error_lines = filter_errors(log_lines)
parsed_errors = parse_error_lines(error_lines)
results = group_by_hour(parsed_errors)
print(results)Diese Pipeline verarbeitet potenziell riesige Logfiles mit minimalem Speicherverbrauch – bis zur finalen Aggregationsstufe befindet sich immer nur eine Zeile im Speicher.
Ein weiteres Beispiel mit Datentransformation:
# Pipeline: numbers -> square -> filter evens -> sum
def square_numbers(numbers):
for n in numbers:
yield n * n
def filter_even(numbers):
for n in numbers:
if n % 2 == 0:
yield n
# Chain the pipeline
numbers = range(1, 11) # 1-10
squared = square_numbers(numbers)
evens = filter_even(squared)
result = sum(evens) # Only even squares
print(result) # 220 (4 + 16 + 36 + 64 + 100)Speichervergleich: Generator vs. List Benchmark
Lass uns einen praxisnahen Benchmark für Speicher und Performance durchführen, um die Vorteile von Generatoren zu quantifizieren:
import sys
import time
import tracemalloc
def process_with_list(n):
"""Traditional approach using lists"""
tracemalloc.start()
start_time = time.time()
# Create list of squares
squares = [x * x for x in range(n)]
# Filter even squares
even_squares = [x for x in squares if x % 2 == 0]
# Sum results
result = sum(even_squares)
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
elapsed = time.time() - start_time
return result, peak / 1024 / 1024, elapsed # Convert to MB
def process_with_generator(n):
"""Generator approach"""
tracemalloc.start()
start_time = time.time()
# Generator pipeline
squares = (x * x for x in range(n))
even_squares = (x for x in squares if x % 2 == 0)
result = sum(even_squares)
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
elapsed = time.time() - start_time
return result, peak / 1024 / 1024, elapsed
# Benchmark with 1 million numbers
n = 1000000
list_result, list_memory, list_time = process_with_list(n)
gen_result, gen_memory, gen_time = process_with_generator(n)
print(f"Results match: {list_result == gen_result}")
print(f"\nList approach:")
print(f" Memory: {list_memory:.2f} MB")
print(f" Time: {list_time:.4f} seconds")
print(f"\nGenerator approach:")
print(f" Memory: {gen_memory:.2f} MB")
print(f" Time: {gen_time:.4f} seconds")
print(f"\nMemory savings: {((list_memory - gen_memory) / list_memory * 100):.1f}%")Typische Ausgabe:
Results match: True
List approach:
Memory: 36.21 MB
Time: 0.0892 seconds
Generator approach:
Memory: 0.12 MB
Time: 0.0624 seconds
Memory savings: 99.7%Der Generator-Ansatz nutzt 99,7% weniger Speicher und ist 30% schneller – ein dramatischer Unterschied, der sich mit größeren Datensätzen weiter verstärkt.
Das itertools-Modul: Generator-Utilities
Pythons itertools-Modul bietet eine Sammlung mächtiger generatorbasierter Tools für effiziente Iteration. Diese Utilities sind in C geschrieben und stark optimiert:
Wichtige itertools-Funktionen
import itertools
# chain - concatenate multiple iterables
combined = itertools.chain([1, 2], [3, 4], [5, 6])
print(list(combined)) # [1, 2, 3, 4, 5, 6]
# islice - slice an iterable (like list slicing but for generators)
numbers = itertools.count() # Infinite counter: 0, 1, 2, 3...
first_ten = itertools.islice(numbers, 10)
print(list(first_ten)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# count - infinite counter with start and step
counter = itertools.count(start=10, step=2)
print([next(counter) for _ in range(5)]) # [10, 12, 14, 16, 18]
# cycle - infinite repetition of an iterable
colors = itertools.cycle(['red', 'green', 'blue'])
print([next(colors) for _ in range(7)]) # ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']
# accumulate - cumulative sums or other operations
numbers = [1, 2, 3, 4, 5]
cumulative = itertools.accumulate(numbers)
print(list(cumulative)) # [1, 3, 6, 10, 15]
# accumulate with custom function
import operator
products = itertools.accumulate(numbers, operator.mul)
print(list(products)) # [1, 2, 6, 24, 120]
# groupby - group consecutive elements by key
data = [('A', 1), ('A', 2), ('B', 3), ('B', 4), ('C', 5)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
print(f"{key}: {list(group)}")
# A: [('A', 1), ('A', 2)]
# B: [('B', 3), ('B', 4)]
# C: [('C', 5)]Praktische itertools-Kombinationen
# Paginating results with islice
def paginate(iterable, page_size):
iterator = iter(iterable)
while True:
page = list(itertools.islice(iterator, page_size))
if not page:
break
yield page
# Usage
data = range(25)
for page_num, page in enumerate(paginate(data, 10), 1):
print(f"Page {page_num}: {page}")
# Page 1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Page 2: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# Page 3: [20, 21, 22, 23, 24]
# Windowed iteration (sliding window)
def window(iterable, size):
it = iter(iterable)
win = list(itertools.islice(it, size))
if len(win) == size:
yield tuple(win)
for item in it:
win = win[1:] + [item]
yield tuple(win)
print(list(window([1, 2, 3, 4, 5], 3)))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]Praxisnahe Use Cases
Große Dateien Zeile für Zeile lesen
def process_large_csv(filename):
"""Process a multi-GB CSV file efficiently"""
with open(filename, 'r') as f:
# Skip header
next(f)
for line in f:
# Parse and yield record
fields = line.strip().split(',')
yield {
'user_id': fields[0],
'action': fields[1],
'timestamp': fields[2]
}
# Process millions of records with minimal memory
for record in process_large_csv('user_events.csv'):
# Process one record at a time
if record['action'] == 'purchase':
print(f"Purchase by user {record['user_id']}")Streaming-Datenverarbeitung
def stream_api_data(url, batch_size=100):
"""Stream paginated API data without loading all results"""
offset = 0
while True:
response = requests.get(url, params={'offset': offset, 'limit': batch_size})
data = response.json()
if not data:
break
for item in data:
yield item
offset += batch_size
# Process unlimited API results
for item in stream_api_data('https://api.example.com/records'):
process_item(item)Iteration über Datenbank-Query-Ergebnisse
def fetch_users_batch(cursor, batch_size=1000):
"""Fetch database records in batches without loading all into memory"""
while True:
results = cursor.fetchmany(batch_size)
if not results:
break
for row in results:
yield row
# Database query
cursor.execute("SELECT * FROM users WHERE active = 1")
# Process millions of users efficiently
for user in fetch_users_batch(cursor):
send_email(user['email'], generate_report(user))ETL-Pipeline-Beispiel
# Extract: Read from source
def extract_from_csv(filename):
with open(filename, 'r') as f:
for line in f:
yield line.strip().split(',')
# Transform: Clean and convert data
def transform_records(records):
for record in records:
yield {
'id': int(record[0]),
'name': record[1].title(),
'email': record[2].lower(),
'age': int(record[3]) if record[3] else None
}
# Load: Write to database
def load_to_database(records, db_connection):
for record in records:
db_connection.execute(
"INSERT INTO users VALUES (?, ?, ?, ?)",
(record['id'], record['name'], record['email'], record['age'])
)
yield record # Pass through for logging
# Build ETL pipeline
raw_data = extract_from_csv('users.csv')
transformed = transform_records(raw_data)
loaded = load_to_database(transformed, db_conn)
# Execute pipeline and count processed records
processed_count = sum(1 for _ in loaded)
print(f"Processed {processed_count} records")Best Practices und häufige Fallstricke bei Generatoren
Best Practices
-
Generator-Expressions für einfache Fälle nutzen
# Simple transformation - use generator expression squares = (x * x for x in range(1000)) # Complex logic - use generator function def complex_processing(data): for item in data: # Multi-step processing result = step1(item) result = step2(result) if validate(result): yield result -
Generatoren für Daten-Pipelines verketten
# Each stage processes lazily data = read_source() filtered = filter_stage(data) transformed = transform_stage(filtered) results = aggregate_stage(transformed) -
yield fromfür Delegation verwendendef process_all_files(directory): for filename in os.listdir(directory): yield from process_file(filename)
Häufige Fallstricke
-
Generatoren sind nach einer Iteration erschöpft
gen = (x for x in range(3)) print(list(gen)) # [0, 1, 2] print(list(gen)) # [] - exhausted! # Solution: Convert to list or recreate generator data = list(gen) # If data fits in memory # OR gen = (x for x in range(3)) # Recreate -
Generatoren unterstützen weder len() noch Indexzugriff
gen = (x for x in range(10)) # len(gen) # TypeError # gen[5] # TypeError # Solution: Convert to list if you need these operations items = list(gen) print(len(items)) print(items[5]) -
Vorsicht bei Scope und Closures mit Generatoren
# Wrong - all generators will use final value of i generators = [lambda: i for i in range(3)] print([g() for g in generators]) # [2, 2, 2] # Correct - capture i in default argument generators = [lambda i=i: i for i in range(3)] print([g() for g in generators]) # [0, 1, 2] -
Exception Handling in Generator-Chains
def stage1(): for i in range(5): if i == 3: raise ValueError("Error in stage1") yield i def stage2(data): try: for item in data: yield item * 2 except ValueError as e: print(f"Caught: {e}") yield -1 # Error marker # Exception is caught in stage2 for result in stage2(stage1()): print(result)
Vergleich: Generatoren vs. Listen vs. Iteratoren vs. map/filter
| Feature | Generators | Lists | Iterators | map/filter |
|---|---|---|---|---|
| Memory usage | Minimal (lazy) | Full dataset | Minimal (lazy) | Minimal (lazy) |
| Creation speed | Instant | Depends on size | Instant | Instant |
| Reusable | No | Yes | No | No |
| Indexable | No | Yes | No | No |
| len() support | No | Yes | No | No |
| Modification | Read-only | Mutable | Read-only | Read-only |
| Infinite sequences | Yes | No | Yes | Yes |
| Syntax | yield or () | [] | iter() | map(), filter() |
| Best for | Large datasets, pipelines | Small datasets, random access | Protocol implementation | Functional transformations |
Beispielvergleich:
# All produce same results but with different characteristics
data = range(1000000)
# Generator - memory efficient, not reusable
gen = (x * 2 for x in data)
# List - memory intensive, reusable, indexable
lst = [x * 2 for x in data]
# map - memory efficient, functional style
mapped = map(lambda x: x * 2, data)
# Iterator - explicit protocol implementation
class Doubler:
def __init__(self, data):
self.data = iter(data)
def __iter__(self):
return self
def __next__(self):
return next(self.data) * 2
iterator = Doubler(data)Generatoren in Jupyter ausprobieren
Wenn du Generator-Patterns und Performance-Eigenschaften erkundest, beschleunigt eine interaktive Notebook-Umgebung das Lernen erheblich. RunCell (opens in a new tab) bringt KI-gestützte Unterstützung direkt in Jupyter-Notebooks und ist damit ideal für Data Scientists, die mit generatorbasierten Data-Processing-Pipelines experimentieren.
Mit RunCell kannst du:
- Generator-Funktionen schnell prototypisieren und Speicherverhalten testen
- Generator-vs-List-Performance mit realen Datensätzen benchmarken
- Komplexe Generator-Pipelines interaktiv bauen und debuggen
- KI-Vorschläge zur Optimierung generatorbasierter ETL-Workflows erhalten
So könntest du Generatoren in einem Notebook erkunden:
# Cell 1: Define generator pipeline
def read_data():
for i in range(1000000):
yield {'id': i, 'value': i * 2}
def filter_large(records):
for record in records:
if record['value'] > 1000:
yield record
def transform(records):
for record in records:
record['squared'] = record['value'] ** 2
yield record
# Cell 2: Execute pipeline and measure
import time
start = time.time()
pipeline = transform(filter_large(read_data()))
results = list(itertools.islice(pipeline, 100)) # Take first 100
print(f"Time: {time.time() - start:.4f}s")
print(f"Results: {len(results)}")
# Cell 3: Visualize with PyGWalker
import pygwalker as pyg
pyg.walk(results)FAQ
Fazit
Python-Generatoren stehen für einen grundlegenden Wechsel von eager zu lazy evaluation und ermöglichen speichereffiziente Verarbeitung von Datensätzen – von Tausenden bis zu Milliarden Records. Wenn du yield, Generator-Expressions, das Iterator-Protokoll und fortgeschrittene Features wie send() und yield from verstehst, kannst du ausgefeilte Datenverarbeitungs-Pipelines bauen, die mühelos skalieren.
Die wichtigsten Erkenntnisse:
- Generatoren nutzen Lazy Evaluation, um den Speicherbedarf zu minimieren – oft 99%+ Ersparnis gegenüber Listen
- Nutze Generator-Expressions für einfache Transformationen, Generator-Funktionen für komplexe Logik
- Verkette Generatoren, um speichereffiziente Datenverarbeitungs-Pipelines zu bauen
- Nutze
itertoolsfür leistungsstarke generatorbasierte Iterations-Utilities - Wähle Generatoren für große Datensätze und Single-Pass-Iteration; wähle Listen für kleine Datensätze mit Random Access
Ob du riesige Logfiles verarbeitest, API-Daten streamst oder ETL-Pipelines baust: Generatoren liefern die Performance und Speichereffizienz, die du für produktionsreife Datenverarbeitung im großen Maßstab brauchst. Wenn du diese Patterns beherrschst, schreibst du eleganten Python-Code, der Datensätze jeder Größe effizient bewältigt.