Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

Les Générateurs en Python : Gérer Efficacement les Grandes Quantités de Données

Hi, dans cet article tu découvres comment les générateurs Python te permettent de traiter efficacement d’énormes flux de données ou des séquences infinies, tout en réduisant drastiquement l’usage de la mémoire grâce au calcul paresseux et à des pipelines élégants.

M
Mpia
12/21/2025 42 min
77 vues
0 comme.
#Python#Generators#Performance
Les Générateurs en Python : Gérer Efficacement les Grandes Quantités de Données

Salut ! Dans cet article, je t'explique comment les générateurs peuvent sauver ta vie quand tu travailles avec plein de données. T'as déjà vu ton programme planter parce qu'il essayait de charger un fichier de plusieurs giga-octets en mémoire ? Ou tu as voulu créer une séquence infinie sans faire exploser ta RAM ? Bienvenue au club ! Les générateurs sont exactement ce qu'il te faut. On va explorer ensemble comment ils fonctionnent vraiment, et tu vas voir comment ils vont transformer ta façon de gérer les données.

Le problème : la mémoire est limitée

Un exemple qui pose problème

Imagine que tu veux traiter les carrés des nombres de 0 à 1 million :

# Approche naïve avec une liste
def creer_carres(n):
    resultat = []
    for i in range(n):
        resultat.append(i ** 2)
    return resultat

# Ou avec une compréhension de liste
carres = [i ** 2 for i in range(1000000)]

# Problème : tout est en mémoire !
# Pour 1 million de nombres, ça fait environ 8 MB
# Pour 100 millions ? 800 MB...

Le problème : Python crée toute la liste en mémoire d'un coup, même si tu n'as besoin que d'un élément à la fois.

La solution : les générateurs

# Générateur : crée les valeurs à la demande
def generer_carres(n):
    for i in range(n):
        yield i ** 2

# Utilisation
carres = generer_carres(1000000)

# Aucune valeur n'est calculée tant qu'on ne les demande pas
for carre in carres:
    if carre > 100:
        break  # On peut s'arrêter quand on veut
    print(carre)

L'avantage : Le générateur ne calcule et ne stocke qu'une valeur à la fois. Tu peux traiter des milliards de valeurs sans problème de mémoire !

Qu'est-ce qu'un générateur ?

La définition

Un générateur est une fonction spéciale qui produit une séquence de valeurs au lieu de retourner une seule valeur. Au lieu d'utiliser return, elle utilise le mot-clé yield.

Différence clé : return vs yield

# Fonction normale avec return
def fonction_normale():
    return 1
    return 2  # Jamais atteint
    return 3  # Jamais atteint

resultat = fonction_normale()
print(resultat)  # 1

# Générateur avec yield
def generateur():
    yield 1
    yield 2
    yield 3

gen = generateur()
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

Ce qui se passe :

  • return termine immédiatement la fonction et renvoie une valeur

  • yield suspend la fonction, retourne une valeur, et reprend là où elle s'était arrêtée au prochain appel

Visualisons l'exécution

def compteur(n):
    print("Début du générateur")
    i = 0
    while i < n:
        print(f"Avant yield {i}")
        yield i
        print(f"Après yield {i}")
        i += 1
    print("Fin du générateur")

# Création du générateur
gen = compteur(3)
print("Générateur créé")

# Premier appel
print(f"Valeur : {next(gen)}")
print("---")

# Deuxième appel
print(f"Valeur : {next(gen)}")
print("---")

# Troisième appel
print(f"Valeur : {next(gen)}")
print("---")

# Quatrième appel (provoque StopIteration)
try:
    print(f"Valeur : {next(gen)}")
except StopIteration:
    print("Générateur épuisé!")

Résultat :

Générateur créé
Début du générateur
Avant yield 0
Valeur : 0
---
Après yield 0
Avant yield 1
Valeur : 1
---
Après yield 1
Avant yield 2
Valeur : 2
---
Après yield 2
Fin du générateur
Générateur épuisé!

Tu vois comment l'exécution se suspend et reprend à chaque yield ? C'est la magie des générateurs !

Créer des générateurs : trois méthodes

Tu as trois façons de créer des générateurs :

Méthode 1 : Fonction avec yield

La méthode la plus simple est d'utiliser le mot-clé yield dans la fonction.

def fibonacci(n):
    """Génère les n premiers nombres de Fibonacci."""
    a, b = 0, 1
    count = 0
    while count < n:
        yield a
        a, b = b, a + b
        count += 1

# Utilisation
for nombre in fibonacci(10):
    print(nombre, end=" ")
# 0 1 1 2 3 5 8 13 21 34

Méthode 2 : Expression génératrice

Comme une compréhension de liste, mais avec des parenthèses :

# Compréhension de liste (crée toute la liste)
carres_liste = [x ** 2 for x in range(10)]

# Expression génératrice (calcule à la demande)
carres_gen = (x ** 2 for x in range(10))

print(type(carres_liste))  # <class 'list'>
print(type(carres_gen))    # <class 'generator'>

# Utilisation identique
for carre in carres_gen:
    print(carre, end=" ")
# 0 1 4 9 16 25 36 49 64 81

Méthode 3 : Classe avec iter et next

Pour un contrôle total (rarement nécessaire), on utilise une classe avec deux méthodes spéciales __iter__ et __next__:

class Compteur:
    def __init__(self, max):
        self.max = max
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.max:
            raise StopIteration

        self.current += 1
        return self.current

# Utilisation
for i in Compteur(5):
    print(i, end=" ")
# 1 2 3 4 5

En pratique, tu vas utiliser presque toujours les méthodes 1 ou 2.

Avantages des générateurs

1. Économie de mémoire

import sys

# Liste : tout en mémoire
liste = [i for i in range(1000000)]
print(f"Taille de la liste : {sys.getsizeof(liste):,} bytes")

# Générateur : presque rien en mémoire
gen = (i for i in range(1000000))
print(f"Taille du générateur : {sys.getsizeof(gen):,} bytes")

Résultat :

Taille de la liste : 8,448,728 bytes (~8 MB)
Taille du générateur : 104 bytes

La différence est dingue ! Le générateur est plus de 80 000 fois plus petit.

2. Calcul paresseux (lazy evaluation)

Les valeurs ne sont calculées que quand tu en as besoin :

def nombres_avec_log(n):
    """Générateur qui log chaque calcul."""
    for i in range(n):
        print(f"Calcul de {i}")
        yield i ** 2

# Création : aucun calcul
gen = nombres_avec_log(5)
print("Générateur créé, aucun calcul effectué")

# On prend juste les 2 premiers
print("\nRécupération des 2 premiers :")
premier = next(gen)
deuxieme = next(gen)

print(f"\nRésultats : {premier}, {deuxieme}")
# Les 3 autres ne sont jamais calculés !

Résultat :

Générateur créé, aucun calcul effectué

Récupération des 2 premiers :
Calcul de 0
Calcul de 1

Résultats : 0, 1

3. Séquences infinies

Tu peux créer des générateurs qui ne se terminent jamais :

def nombres_impairs():
    """Générateur infini de nombres impairs."""
    n = 1
    while True:  # Boucle infinie !
        yield n
        n += 2

# Utilisation safe avec une limite
impairs = nombres_impairs()
for _ in range(10):
    print(next(impairs), end=" ")
# 1 3 5 7 9 11 13 15 17 19

Impossible de faire ça avec une liste !

Application pratique 1 : Lire un gros fichier

Le problème avec l'approche classique

# ❌ Mauvaise approche : charge tout en mémoire
def traiter_fichier_mauvais(nom_fichier):
    with open(nom_fichier, 'r') as f:
        lignes = f.readlines()  # Tout le fichier en RAM !
        for ligne in lignes:
            traiter_ligne(ligne)

Si le fichier fait 10 GB, ton programme va planter.

Solution avec un générateur

def lire_fichier_par_chunks(nom_fichier, taille_chunk=1024):
    """Lit un fichier par morceaux."""
    with open(nom_fichier, 'r') as f:
        while True:
            chunk = f.read(taille_chunk)
            if not chunk:
                break
            yield chunk

# Utilisation
for chunk in lire_fichier_par_chunks('gros_fichier.txt'):
    traiter_chunk(chunk)
    # Un seul chunk en mémoire à la fois !

Traiter les lignes une par une

def lire_lignes(nom_fichier):
    """Générateur qui lit un fichier ligne par ligne."""
    with open(nom_fichier, 'r', encoding='utf-8') as f:
        for ligne in f:  # f est déjà un générateur !
            yield ligne.strip()

# Filtrer les lignes vides
def lignes_non_vides(nom_fichier):
    for ligne in lire_lignes(nom_fichier):
        if ligne:
            yield ligne

# Compter les mots dans un fichier énorme
def compter_mots(nom_fichier):
    total = 0
    for ligne in lignes_non_vides(nom_fichier):
        total += len(ligne.split())
    return total

# Fonctionne même avec des fichiers de plusieurs GB !

Application pratique 2 : Pipeline de traitement de données

Les générateurs sont parfaits pour créer des pipelines de transformation de données.

Exemple : Analyser des logs

def lire_logs(nom_fichier):
    """Lit un fichier de logs."""
    with open(nom_fichier, 'r') as f:
        for ligne in f:
            yield ligne.strip()

def filtrer_erreurs(lignes):
    """Garde seulement les lignes d'erreur."""
    for ligne in lignes:
        if 'ERROR' in ligne:
            yield ligne

def extraire_timestamp(lignes):
    """Extrait le timestamp de chaque ligne."""
    for ligne in lignes:
        # Format: "2024-01-15 10:30:45 ERROR ..."
        parties = ligne.split()
        if len(parties) >= 2:
            yield f"{parties[0]} {parties[1]}"

def compter_par_heure(timestamps):
    """Compte les erreurs par heure."""
    from collections import Counter
    compteur = Counter()

    for timestamp in timestamps:
        heure = timestamp.split()[1].split(':')[0]  # Extrait l'heure
        compteur[heure] += 1

    return compteur

# Pipeline complet
logs = lire_logs('application.log')
erreurs = filtrer_erreurs(logs)
timestamps = extraire_timestamp(erreurs)
resultats = compter_par_heure(timestamps)

print("Erreurs par heure :")
for heure, count in sorted(resultats.items()):
    print(f"{heure}h: {count} erreurs")

L'avantage : Chaque étape est un générateur. On traite les données en flux, sans jamais charger tout le fichier en mémoire !

Application pratique 3 : Générer des données de test

import random
from datetime import datetime, timedelta

def generer_utilisateurs(n):
    """Génère n utilisateurs de test."""
    prenoms = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
    noms = ["Martin", "Dupont", "Bernard", "Petit", "Robert"]

    for i in range(n):
        prenom = random.choice(prenoms)
        nom = random.choice(noms)
        email = f"{prenom.lower()}.{nom.lower()}{i}@mail.com"
        age = random.randint(18, 70)

        yield {
            "id": i + 1,
            "nom": f"{prenom} {nom}",
            "email": email,
            "age": age
        }

def generer_transactions(utilisateurs, nb_transactions):
    """Génère des transactions pour les utilisateurs."""
    date_debut = datetime.now() - timedelta(days=365)

    for i in range(nb_transactions):
        utilisateur = random.choice(list(utilisateurs))
        montant = round(random.uniform(10, 1000), 2)
        date = date_debut + timedelta(days=random.randint(0, 365))

        yield {
            "id": i + 1,
            "utilisateur_id": utilisateur["id"],
            "montant": montant,
            "date": date.strftime("%Y-%m-%d")
        }

# Utilisation
utilisateurs = list(generer_utilisateurs(100))
transactions = generer_transactions(utilisateurs, 1000)

# On peut maintenant traiter les transactions une par une
montant_total = sum(t["montant"] for t in transactions)
print(f"Montant total : {montant_total:.2f}€")

Chaîner des générateurs

Tu peux combiner plusieurs générateurs pour créer des transformations complexes.

Exemple : Traitement de nombres

def nombres(n):
    """Génère les nombres de 1 à n."""
    for i in range(1, n + 1):
        yield i

def filtrer_pairs(nombres):
    """Garde seulement les nombres pairs."""
    for n in nombres:
        if n % 2 == 0:
            yield n

def multiplier(nombres, facteur):
    """Multiplie chaque nombre par un facteur."""
    for n in nombres:
        yield n * facteur

def limiter(nombres, max_elements):
    """Limite le nombre d'éléments."""
    count = 0
    for n in nombres:
        if count >= max_elements:
            break
        yield n
        count += 1

# Pipeline
nums = nombres(100)
pairs = filtrer_pairs(nums)
doubles = multiplier(pairs, 2)
premiers = limiter(doubles, 5)

print(list(premiers))  # [4, 8, 12, 16, 20]

Version plus élégante avec des expressions génératrices

# Tout en une ligne !
resultat = (
    n * 2 
    for n in range(1, 101) 
    if n % 2 == 0
)

# Prendre les 5 premiers
print(list(limiter(resultat, 5)))  # [4, 8, 12, 16, 20]

yield from : déléguer à un autre générateur

yield from permet de déléguer la production de valeurs à un autre générateur.

Sans yield from

def sous_generateur():
    yield 1
    yield 2
    yield 3

def generateur_principal():
    # Méthode verbeuse
    for valeur in sous_generateur():
        yield valeur

    yield 4
    yield 5

print(list(generateur_principal()))  # [1, 2, 3, 4, 5]

Avec yield from

def generateur_principal():
    yield from sous_generateur()  # Plus simple !
    yield 4
    yield 5

print(list(generateur_principal()))  # [1, 2, 3, 4, 5]

Exemple pratique : Aplatir une structure imbriquée

def aplatir(structure):
    """Aplatit une structure imbriquée de listes."""
    for element in structure:
        if isinstance(element, list):
            yield from aplatir(element)  # Récursif !
        else:
            yield element

# Test
nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(list(aplatir(nested)))  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Envoyer des valeurs dans un générateur

Les générateurs peuvent aussi recevoir des valeurs avec la méthode send().

Exemple basique

def echo():
    """Générateur qui répète ce qu'on lui envoie."""
    while True:
        valeur = yield
        if valeur is not None:
            print(f"Reçu : {valeur}")

gen = echo()
next(gen)  # Initialiser le générateur

gen.send("Hello")   # Reçu : Hello
gen.send("World")   # Reçu : World
gen.send(42)        # Reçu : 42

Exemple pratique : Calculateur de moyenne mobile

def moyenne_mobile(fenetre=3):
    """Calcule une moyenne mobile."""
    valeurs = []
    while True:
        nouvelle_valeur = yield sum(valeurs) / len(valeurs) if valeurs else 0
        valeurs.append(nouvelle_valeur)

        # Garder seulement les N dernières valeurs
        if len(valeurs) > fenetre:
            valeurs.pop(0)

# Utilisation
calc = moyenne_mobile(3)
next(calc)  # Initialiser

print(calc.send(10))   # 10.0
print(calc.send(20))   # 15.0
print(calc.send(30))   # 20.0
print(calc.send(40))   # 30.0 (moyenne de 20, 30, 40)
print(calc.send(50))   # 40.0 (moyenne de 30, 40, 50)

Générateurs vs Listes : Quand utiliser quoi ?

Utilisez une liste quand :

# ✅ Tu dois accéder aux éléments plusieurs fois
resultats = [calcul_couteux(x) for x in range(10)]
print(resultats[0])
print(resultats[5])
for r in resultats:  # Deuxième itération
    print(r)

# ✅ Tu as besoin de la longueur
nombres = [i for i in range(100)]
print(len(nombres))  # Possible avec une liste

# ✅ Tu veux accéder par index
nombres[50]  # Possible avec une liste

# ✅ Les données sont petites
petite_liste = [i ** 2 for i in range(100)]  # OK

Utilisez un générateur quand :

# ✅ Les données sont volumineuses
gros_fichier = (ligne for ligne in open('huge.txt'))

# ✅ Tu n'itères qu'une seule fois
for ligne in lire_fichier('data.txt'):
    traiter(ligne)

# ✅ Tu veux du calcul paresseux
resultats = (calcul_couteux(x) for x in range(1000000))
premier = next(resultats)  # Calcule seulement le premier

# ✅ Tu crées des séquences infinies
def nombres_premiers():
    # Génère tous les nombres premiers (infini)
    ...

# ✅ Pipeline de transformations
pipeline = (
    transform3(x)
    for x in transform2(y)
    for y in transform1(z)
    for z in source_data
)

Performances : Benchmark

Comparons les performances en pratique :

import time
import sys

def benchmark_liste(n):
    """Créer une liste."""
    start = time.time()
    liste = [i ** 2 for i in range(n)]
    somme = sum(liste)
    duree = time.time() - start
    taille = sys.getsizeof(liste)
    return duree, taille, somme

def benchmark_generateur(n):
    """Créer un générateur."""
    start = time.time()
    gen = (i ** 2 for i in range(n))
    somme = sum(gen)
    duree = time.time() - start
    taille = sys.getsizeof(gen)
    return duree, taille, somme

n = 1000000

duree_l, taille_l, somme_l = benchmark_liste(n)
duree_g, taille_g, somme_g = benchmark_generateur(n)

print(f"Liste :")
print(f"  Temps : {duree_l:.4f}s")
print(f"  Mémoire : {taille_l:,} bytes")
print(f"  Somme : {somme_l:,}")

print(f"\nGénérateur :")
print(f"  Temps : {duree_g:.4f}s")
print(f"  Mémoire : {taille_g:,} bytes")
print(f"  Somme : {somme_g:,}")

print(f"\nGain mémoire : {taille_l / taille_g:.1f}x")

Résultat typique :

Liste :
  Temps : 0.0891s
  Mémoire : 8,697,464 bytes
  Somme : 333,332,833,333,500,000

Générateur :
  Temps : 0.0623s
  Mémoire : 104 bytes
  Somme : 333,332,833,333,500,000

Gain mémoire : 83,629.5x

Le générateur est plus rapide ET utilise 83 000 fois moins de mémoire ! C'est fou.

Pièges courants à éviter

Piège 1 : Un générateur s'épuise

gen = (i for i in range(3))

# Première itération
for i in gen:
    print(i)  # 0, 1, 2

# Deuxième itération
for i in gen:
    print(i)  # Rien ! Le générateur est épuisé

# Solution : recréer le générateur
gen = (i for i in range(3))

Piège 2 : Pas de len() sur un générateur

gen = (i for i in range(100))

# ❌ Erreur
# print(len(gen))  # TypeError

# ✅ Solution : convertir en liste (si petit)
liste = list(gen)
print(len(liste))

Piège 3 : Pas d'accès par index

gen = (i ** 2 for i in range(10))

# ❌ Erreur
# print(gen[5])  # TypeError

# ✅ Solutions
# 1. Convertir en liste
liste = list(gen)
print(liste[5])

# 2. Utiliser itertools.islice
from itertools import islice
gen = (i ** 2 for i in range(10))
cinquieme = next(islice(gen, 5, 6))
print(cinquieme)

Outils utiles du module itertools

Le module itertools offre plein d'outils pratiques pour travailler avec les générateurs.

islice : Découper un générateur

from itertools import islice

def nombres():
    i = 0
    while True:
        yield i
        i += 1

# Prendre les éléments 10 à 14
gen = nombres()
subset = islice(gen, 10, 15)
print(list(subset))  # [10, 11, 12, 13, 14]

chain : Chaîner des générateurs

from itertools import chain

gen1 = (i for i in range(3))
gen2 = (i for i in range(10, 13))

combine = chain(gen1, gen2)
print(list(combine))  # [0, 1, 2, 10, 11, 12]

takewhile et dropwhile

from itertools import takewhile, dropwhile

nombres = (i for i in range(10))

# Prendre tant que < 5
petits = takewhile(lambda x: x < 5, nombres)
print(list(petits))  # [0, 1, 2, 3, 4]

# Ignorer tant que < 5, puis tout prendre
nombres = (i for i in range(10))
grands = dropwhile(lambda x: x < 5, nombres)
print(list(grands))  # [5, 6, 7, 8, 9]

cycle et repeat

from itertools import cycle, repeat

# Répéter une séquence indéfiniment
couleurs = cycle(['rouge', 'vert', 'bleu'])
for i, couleur in enumerate(couleurs):
    if i >= 7:
        break
    print(couleur, end=" ")
# rouge vert bleu rouge vert bleu rouge

# Répéter une valeur
trois = repeat(3, times=5)
print(list(trois))  # [3, 3, 3, 3, 3]

Exercices pratiques

Exercice 1 : Générateur de nombres premiers

Crée un générateur qui produit tous les nombres premiers.

Exercice 2 : Générateur de permutations

Crée un générateur qui produit toutes les permutations d'une liste.

Exercice 3 : Parser CSV

Crée un générateur qui lit un fichier CSV et retourne des dictionnaires.

Exercice 4 : Pagination

Crée un générateur qui découpe une liste en pages de taille N.

Solutions

Cliquez pour voir les solutions
# Solution 1 : Nombres premiers
def nombres_premiers():
    """Génère tous les nombres premiers."""
    def est_premier(n):
        if n < 2:
            return False
        for i in range(2, int(n ** 0.5) + 1):
            if n % i == 0:
                return False
        return True

    n = 2
    while True:
        if est_premier(n):
            yield n
        n += 1

# Test
premiers = nombres_premiers()
print([next(premiers) for _ in range(10)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

# Solution 2 : Permutations
def permutations(liste):
    """Génère toutes les permutations."""
    if len(liste) <= 1:
        yield liste
    else:
        for i in range(len(liste)):
            element = liste[i]
            reste = liste[:i] + liste[i+1:]
            for p in permutations(reste):
                yield [element] + p

# Test
print(list(permutations([1, 2, 3])))
# [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

# Solution 3 : Parser CSV
def parser_csv(nom_fichier):
    """Parse un CSV et retourne des dictionnaires."""
    with open(nom_fichier, 'r') as f:
        # Lire la première ligne (headers)
        headers = next(f).strip().split(',')

        # Lire les lignes de données
        for ligne in f:
            valeurs = ligne.strip().split(',')
            yield dict(zip(headers, valeurs))

# Solution 4 : Pagination
def paginer(liste, taille_page):
    """Découpe une liste en pages."""
    for i in range(0, len(liste), taille_page):
        yield liste[i:i + taille_page]

# Test
donnees = list(range(25))
for i, page in enumerate(paginer(donnees, 10)):
    print(f"Page {i + 1}: {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]

Conclusion

Les générateurs sont un outil absolument indispensable pour tout développeur Python qui bosse avec des données volumineuses ou qui veut vraiment optimiser l'utilisation de la mémoire. Ils permettent d'écrire du code élégant, performant et qui scale vraiment bien.

Les points clés à retenir :

  1. yield suspend l'exécution et permet de reprendre plus tard

  2. Les générateurs sont paresseux : ils calculent à la demande

  3. Économie de mémoire massive : parfait pour les gros volumes

  4. Ils sont parfaits pour les pipelines de transformation de données

  5. Utilise les expressions génératrices pour la simplicité

  6. itertools offre des outils vraiment puissants pour manipuler les générateurs

  7. Un générateur s'épuise : tu ne peux l'itérer qu'une fois

Commence par remplacer tes listes par des générateurs dans les cas où tu n'as besoin d'itérer qu'une fois. Progressivement, tu vas développer l'instinct de quand les utiliser. Les générateurs vont vraiment changer ta façon de penser le traitement des données en Python !


Curieux d'approfondir d'autres concepts avancés en Python ? Consulte les articles sur Python sur codewithmpia!

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !