Skip to content

Générateurs Python : guide complet de yield, des expressions de générateur et de l’évaluation paresseuse

Updated on

Traiter un fichier de logs de 10 Go ou diffuser (streamer) des millions d’enregistrements issus d’une base de données peut mettre une application Python à genoux. L’approche traditionnelle consistant à charger toutes les données en mémoire d’un coup entraîne des goulots d’étranglement, des erreurs de mémoire et des utilisateurs frustrés. C’est là que les générateurs Python deviennent essentiels : ils vous permettent de traiter d’énormes jeux de données avec une empreinte mémoire minimale, en générant les valeurs à la demande plutôt qu’en stockant tout à l’avance.

📚

Que sont les générateurs Python et pourquoi ils sont importants

Les générateurs sont des fonctions spéciales qui produisent une séquence de valeurs au fil du temps, au lieu de toutes les calculer et de les renvoyer en une seule fois. Contrairement aux fonctions classiques qui utilisent return pour renvoyer un unique résultat, les générateurs utilisent le mot-clé yield pour produire une série de valeurs, en mettant l’exécution en pause entre chaque valeur et en reprenant lorsque la valeur suivante est demandée.

L’avantage fondamental des générateurs est l’évaluation paresseuse : les valeurs ne sont générées que lorsqu’elles sont nécessaires. Cela apporte deux bénéfices critiques :

  1. Efficacité mémoire : les générateurs ne stockent pas la séquence entière en mémoire. Un générateur produisant un milliard de nombres consomme la même mémoire qu’un générateur en produisant dix.
  2. Performances : le traitement peut commencer immédiatement dès la première valeur produite, sans attendre que l’ensemble du jeu de données soit préparé.

Voici une comparaison simple illustrant la différence :

# 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

La différence mémoire est spectaculaire : dans cet exemple, le générateur utilise 99,999 % de mémoire en moins que la liste. Et cet écart s’amplifie fortement avec des jeux de données plus volumineux.

Le mot-clé yield : le cœur des fonctions génératrices

Le mot-clé yield est ce qui transforme une fonction classique en fonction génératrice. Lorsque Python rencontre yield, il sait qu’il doit renvoyer un objet générateur au lieu d’exécuter la fonction immédiatement.

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

Comportements clés à comprendre :

  • L’exécution se met en pause à chaque instruction yield et reprend exactement à cet endroit lors de l’appel suivant.
  • Les variables locales conservent leur état entre les appels à yield.
  • L’exception StopIteration est levée lorsque la fonction génératrice se termine (n’a plus de valeurs à produire).

Plusieurs yield peuvent apparaître dans un même générateur :

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)

Protocole des générateurs : comprendre iter() et next()

Les générateurs implémentent le protocole d’itération via deux méthodes spéciales :

  • __iter__() : renvoie l’itérateur lui-même (le générateur)
  • __next__() : renvoie la valeur suivante du générateur

Cela rend les générateurs parfaits pour une utilisation dans les boucles for et autres contextes d’itération. Comprendre ce protocole aide à clarifier le fonctionnement interne :

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

Vous pouvez aussi implémenter manuellement le protocole d’itération pour créer un comportement similaire à un générateur :

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

Cependant, les fonctions génératrices sont bien plus concises et lisibles que des classes d’itérateurs écrites à la main.

Expressions de générateur vs compréhensions de liste

Les expressions de générateur offrent une syntaxe concise pour créer des générateurs, similaire aux compréhensions de liste mais avec des parenthèses au lieu de crochets :

# 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]

Comparaison de syntaxe :

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)

Exemple pratique montrant la différence mémoire :

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

Les expressions de générateur sont idéales lorsque vous n’avez besoin de parcourir les valeurs qu’une seule fois et que vous souhaitez minimiser l’usage mémoire.

yield from : déléguer à des sous-générateurs

L’instruction yield from simplifie la délégation à des sous-générateurs ou à d’autres itérables. Au lieu de boucler manuellement et de yield chaque valeur, yield from s’en charge automatiquement :

# 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]

C’est particulièrement utile pour aplatir des structures imbriquées :

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 gère aussi correctement les exceptions et les valeurs de retour des sous-générateurs, ce qui le rend essentiel pour des pipelines de générateurs complexes.

Avancé : méthodes send() et throw()

Les générateurs peuvent faire plus que produire des valeurs : ils peuvent aussi recevoir des valeurs et gérer des exceptions via les méthodes send() et throw(), permettant une communication bidirectionnelle de type coroutine.

Utiliser send() pour envoyer des valeurs dans les générateurs

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

La méthode send() envoie une valeur dans le générateur (qui devient le résultat de l’expression yield) et avance l’exécution jusqu’au prochain yield.

Utiliser throw() pour injecter des exceptions

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

Ces fonctionnalités avancées sont particulièrement utiles pour implémenter des machines à états, des coroutines et des modèles asynchrones complexes.

Générateurs infinis : des séquences sans fin

Les générateurs excellent pour produire des séquences infinies, car ils n’ont jamais besoin de matérialiser la séquence entière en mémoire :

# 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']

Les générateurs infinis sont particulièrement utiles pour les flux d’événements, la supervision continue et les modèles d’itération à état.

Chaîner des générateurs : construire des pipelines de traitement de données

L’un des patterns les plus puissants avec les générateurs consiste à les chaîner pour créer des pipelines de traitement efficaces. Chaque étape traite les données de manière paresseuse et transmet le résultat à la suivante sans stocker les résultats intermédiaires :

# 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)

Ce pipeline traite un fichier de logs potentiellement énorme avec une utilisation mémoire minimale : seule une ligne est en mémoire à la fois, jusqu’à l’étape finale d’agrégation.

Autre exemple avec transformation de données :

# 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)

Comparaison mémoire : benchmark générateur vs liste

Réaliser un benchmark mémoire et performances en conditions proches du réel permet de quantifier les bénéfices des générateurs :

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}%")

Sortie typique :

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%

L’approche par générateurs utilise 99,7 % de mémoire en moins et s’exécute 30 % plus vite : une amélioration majeure qui se renforce encore avec des jeux de données plus gros.

Le module itertools : utilitaires de générateurs

Le module itertools de Python fournit une collection d’outils puissants basés sur des générateurs pour itérer efficacement. Ces utilitaires sont écrits en C et fortement optimisés.

Fonctions itertools essentielles

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)]

Combinaisons itertools pratiques

# 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)]

Cas d’usage concrets

Lire de gros fichiers ligne par ligne

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']}")

Traitement de données en streaming

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)

Itérer sur les résultats d’une requête base de données

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))

Exemple de pipeline ETL

# 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")

Bonnes pratiques et pièges courants des générateurs

Bonnes pratiques

  1. Utiliser des expressions de générateur pour les cas simples

    # 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. Chaîner des générateurs pour les pipelines de données

    # Each stage processes lazily
    data = read_source()
    filtered = filter_stage(data)
    transformed = transform_stage(filtered)
    results = aggregate_stage(transformed)
  3. Utiliser yield from pour la délégation

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

Pièges courants

  1. Les générateurs sont épuisés après une itération

    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. Les générateurs ne supportent pas len() ni l’indexation

    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. Attention à la portée (scope) et aux fermetures (closures) avec les générateurs

    # 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. Gestion des exceptions dans les chaînes de générateurs

    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)

Comparaison : générateurs vs listes vs itérateurs 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

Exemple de comparaison :

# 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)

Expérimenter avec les générateurs dans Jupyter

Lorsque vous explorez des patterns de générateurs et leurs caractéristiques de performance, travailler dans un environnement de notebook interactif accélère l’apprentissage. RunCell (opens in a new tab) apporte une assistance alimentée par l’IA directement dans les notebooks Jupyter, ce qui en fait un excellent choix pour les data scientists qui expérimentent des pipelines de traitement de données basés sur des générateurs.

Avec RunCell, vous pouvez :

  • Prototyper rapidement des fonctions génératrices et tester leurs caractéristiques mémoire
  • Benchmark les performances générateur vs liste sur des jeux de données réels
  • Construire et déboguer interactivement des pipelines complexes de générateurs
  • Obtenir des suggestions IA pour optimiser des workflows ETL basés sur des générateurs

Voici comment vous pourriez explorer les générateurs dans un notebook :

# 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

Conclusion

Les générateurs Python représentent un changement fondamental : passer d’une évaluation « eager » à une évaluation paresseuse (« lazy »), permettant un traitement économe en mémoire pour des jeux de données allant de milliers à des milliards d’enregistrements. En comprenant yield, les expressions de générateur, le protocole d’itération et des fonctionnalités avancées comme send() et yield from, vous pouvez construire des pipelines de traitement sophistiqués qui passent à l’échelle sans effort.

Les points clés à retenir :

  • Les générateurs s’appuient sur l’évaluation paresseuse pour minimiser l’empreinte mémoire — souvent 99 % et plus d’économies par rapport aux listes
  • Utilisez les expressions de générateur pour les transformations simples et les fonctions génératrices pour la logique complexe
  • Chaînez les générateurs pour construire des pipelines de traitement de données économes en mémoire
  • Exploitez itertools pour des utilitaires d’itération puissants basés sur des générateurs
  • Choisissez les générateurs pour de gros jeux de données et une itération en un seul passage ; choisissez les listes pour de petits jeux de données nécessitant un accès aléatoire

Que vous traitiez des fichiers de logs massifs, streamiez des données d’API ou construisiez des pipelines ETL, les générateurs offrent les performances et l’efficacité mémoire nécessaires au traitement de données à l’échelle production. Maîtrisez ces patterns et vous écrirez du code Python capable de gérer des jeux de données de toute taille avec élégance et efficacité.

📚