Skip to content

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:

  1. 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.
  2. 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 bytes

Der 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 StopIteration

Wichtige 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, 3

Du 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, 1

Allerdings 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:

FeatureList ComprehensionGenerator Expression
Syntax[expr for item in iterable](expr for item in iterable)
ReturnsList objectGenerator object
MemoryStores all valuesGenerates on-demand
SpeedFaster for small datasetsFaster for large datasets
ReusableYes (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 bytes

Generator-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.0

Die 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 closing

Diese 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

  1. 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
  2. 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)
  3. yield from für Delegation verwenden

    def process_all_files(directory):
        for filename in os.listdir(directory):
            yield from process_file(filename)

Häufige Fallstricke

  1. 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
  2. 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])
  3. 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]
  4. 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

FeatureGeneratorsListsIteratorsmap/filter
Memory usageMinimal (lazy)Full datasetMinimal (lazy)Minimal (lazy)
Creation speedInstantDepends on sizeInstantInstant
ReusableNoYesNoNo
IndexableNoYesNoNo
len() supportNoYesNoNo
ModificationRead-onlyMutableRead-onlyRead-only
Infinite sequencesYesNoYesYes
Syntaxyield or ()[]iter()map(), filter()
Best forLarge datasets, pipelinesSmall datasets, random accessProtocol implementationFunctional 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 itertools fü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.

📚