Accéder au contenu principal

Générateurs Python : Accroître les performances et simplifier le code

Découvrez les générateurs Python pour optimiser l'utilisation de la mémoire et simplifier les flux de travail. Créez des pipelines de données efficaces et réalistes et améliorez les performances de votre code.
Actualisé 14 févr. 2025  · 10 min de lecture

Imaginez que vous travaillez sur un projet de science des données et que votre tâche consiste à traiter un ensemble de données si volumineux que son chargement en mémoire fait planter votre machine. Ou bien vous avez affaire à une séquence infinie, comme un flux de données en direct, où il est impossible de tout stocker simultanément. C'est le genre de défis qui poussent les scientifiques des données à se tourner vers la cafetière et, parfois, vers le bouton de réinitialisation.  

Dans cet article, nous allons découvrir les générateurs Python, et comment vous pouvez les utiliser pour simplifier votre code. Cette idée demande un peu de pratique, donc, si vous êtes nouveau à Python et que vous vous sentez un peu perdu dans cet article, essayez notre cours Introduction à Python pour construire une base solide. 

Apprenez Python à partir de zéro

Maîtrisez Python pour la science des données et acquérez des compétences recherchées.
Commencez à apprendre gratuitement

Que sont les générateurs Python ?

À la base, les générateurs Python sont un type spécial de fonction ou même une expression compacte qui produit une séquence de valeurs paresseusement. Pensez aux générateurs comme à un tapis roulant dans une usine : Au lieu d'empiler tous les produits au même endroit et de manquer de place, vous traitez chaque article au fur et à mesure qu'il arrive. Les générateurs sont donc peu gourmands en mémoire et constituent une extension naturelle du protocole Python iterator, qui sous-tend de nombreux outils intégrés à Python, tels que les boucles for et les compréhensions.  

La magie des générateurs réside dans le mot-clé yield. Contrairement à return, qui produit une seule valeur et quitte la fonction, yield produit une valeur, interrompt l'exécution de la fonction et enregistre son état. Lorsque le générateur est appelé à nouveau, il reprend là où il s'est arrêté. 

Imaginez, par exemple, que vous lisiez un énorme fichier journal ligne par ligne. Un générateur peut traiter chaque ligne au fur et à mesure de sa lecture sans charger le fichier entier en mémoire. Cette "évaluation paresseuse" distingue les générateurs des fonctions traditionnelles et en fait un outil de choix pour les tâches sensibles aux performances.  

Un exemple de générateur Python de base

Exerçons-nous un peu pour comprendre l'idée. Voici une fonction génératrice qui produit les premiers n entiers.  

def generate_integers(n):
    for i in range(n):
        yield i  # Pauses here and returns i

# Using the generator
for num in generate_integers(5):
    print(num)
0
1
2
3
4

J'ai créé un visuel pour vous aider à comprendre ce qui se passe sous le capot :  

Organigramme des fonctions du générateur Python

Syntaxe et modèles du générateur Python  

Les générateurs peuvent être mis en œuvre de multiples façons. Cela dit, il existe deux méthodes principales : les fonctions de générateur et les expressions de générateur.

Fonctions du générateur  

Une fonction de générateur est définie comme une fonction normale, mais utilise le mot-clé yield au lieu de return.. Lorsqu'elle est appelée, elle renvoie un objet générateur sur lequel il est possible d'itérer.  

def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

# Using the generator
counter = count_up_to(5)
for num in counter:
    print(num)
1
2
3
4
5

Dans l'exemple ci-dessus, nous pouvons voir que lorsque la fonction count_up_to est appelée, elle renvoie un objet générateur. Chaque fois que la boucle for demande une valeur, la fonction s'exécute jusqu'à ce qu'elle atteigne yield, produisant la valeur actuelle de count et préservant son état entre les itérations afin qu'elle puisse reprendre exactement là où elle s'est arrêtée.

Expressions du générateur  

Les expressions de générateur sont un moyen compact de créer des générateurs. Elles sont similaires aux compréhensions de listes, mais avec des parenthèses à la place des crochets.

# List comprehension (eager evaluation)
squares_list = [x**2 for x in range(5)]  # [0, 1, 4, 9, 16]

# Generator expression (lazy evaluation)
squares_gen = (x**2 for x in range(5))

# Using the generator
for square in squares_gen:
    print(square)
0
1
4
9
16

Quelle est donc la différence entre une compréhension de liste et une expression de générateur ? La compréhension de la liste crée la liste entière en mémoire, tandis que l'expression du générateur produit les valeurs une à une fois, ce qui permet d'économiser de la mémoire. Si vous n'êtes pas familier avec les compréhensions de liste, vous pouvez en prendre connaissance dans notre tutoriel sur les compréhensions de liste en Python.

Générateur et itérateur en Python

Les itérateurs traditionnels en Python nécessitaient des classes avec des méthodes explicites __iter__() et __next__(), ce qui impliquait un grand nombre de pages de garde et une gestion manuelle de l'état, alors que les fonctions génératrices simplifient le processus en préservant automatiquement l'état et en éliminant le besoin de ces méthodes - comme le montre une fonction simple qui donne le carré de chaque nombre jusqu'à n.

Pourquoi nous utilisons des générateurs Python  

En expliquant ce que sont les générateurs Python, j'ai également transmis une partie de l'idée de la raison pour laquelle ils sont utilisés. Dans cette section, je souhaite entrer un peu plus dans les détails. En effet, les générateurs ne sont pas seulement une fonction fantaisiste de Python, mais ils permettent réellement de résoudre des problèmes concrets.

Efficacité de la mémoire  

Contrairement aux listes et aux tableaux, qui stockent simultanément tous leurs éléments en mémoire, les générateurs produisent des valeurs à la volée et ne conservent donc qu'un seul élément en mémoire à la fois.  

Par exemple, considérez la différence entre range() et xrange() de Python 2 :  

  •  range() crée une liste en mémoire, ce qui peut s'avérer problématique pour les grandes plages.

  •  xrange() a agi comme un générateur, produisant des valeurs paresseusement.

Parce que le comportement de xrange() était plus utile, maintenant, dans Python 3, range() se comporte également comme un générateur, de sorte qu'il évite la surcharge de mémoire pour stocker toutes les valeurs simultanément.  

Pour illustrer cette idée, comparons l'utilisation de la mémoire lors de la génération d'une séquence de 10 millions de nombres :  

import sys

# Using a list
numbers_list = [x for x in range(10_000_000)]
print(f"Memory used by list: {sys.getsizeof(numbers_list) / 1_000_000:.2f} MB")

# Using a generator
numbers_gen = (x for x in range(10_000_000))
print(f"Memory used by generator: {sys.getsizeof(numbers_gen)} bytes")
Memory used by list: 89.48 MB
Memory used by the generator: 112 bytes

Comme vous pouvez le constater, le générateur n'utilise pratiquement pas de mémoire par rapport à la liste, et cette différence est significative.  

Amélioration des performances  

Grâce à l'évaluation paresseuse, les valeurs ne sont calculées que lorsque cela est nécessaire. Cela signifie que vous pouvez commencer à traiter les données immédiatement sans attendre que la séquence entière soit générée.  

Imaginez par exemple que vous fassiez la somme des carrés des 1 premiers millions de nombres :

# Using a list (eager evaluation)
sum_of_squares_list = sum([x**2 for x in range(1_000_000)])

# Using a generator (lazy evaluation)
sum_of_squares_gen = sum(x**2 for x in range(1_000_000))

Bien que les deux approches donnent le même résultat, la version du générateur évite de créer une liste massive, ce qui permet d'obtenir le résultat plus rapidement. 

Simplicité et lisibilité  

Les générateurs simplifient la mise en œuvre des itérateurs en éliminant le code de base. Comparez un itérateur basé sur une classe avec une fonction génératrice :  

Voici l'itérateur basé sur la classe :

class SquaresIterator:
    def __init__(self, n):
        self.n = n
        self.current = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.current >= self.n:
            raise StopIteration
        result = self.current ** 2
        self.current += 1
        return result

# Usage
squares = SquaresIterator(5)
for square in squares:
    print(square)

Voici la fonction du générateur :

def squares_generator(n):
    for i in range(n):
        yield i ** 2

# Usage
squares = squares_generator(5)
for square in squares:
    print(square)

La version du générateur est plus courte, plus facile à lire et ne nécessite pas de code de base. C'est un exemple parfait de la philosophie de Python : simple, c'est mieux.  

Traitement des séquences infinies  

Enfin, je tiens à préciser que les générateurs sont particulièrement adaptés à la représentation de séquences infinies, ce qui est tout simplement impossible avec les listes. Prenons l'exemple de la suite de Fibonacci:  

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Usage
fib = fibonacci()
for _ in range(10):
    print(next(fib))
0
1
1
2
3
5
8
13
21
34

Ce générateur peut produire des nombres de Fibonacci indéfiniment sans manquer de mémoire. D'autres exemples incluent le traitement de flux de données en direct ou le travail avec des données de séries temporelles.  

Concepts avancés du générateur Python  

Voyons maintenant quelques idées plus difficiles. Dans cette section, nous verrons comment composer des générateurs et utiliser des méthodes de génération uniques telles que .send(), .throw() et .close().  

Enchaînement de générateurs

Les générateurs peuvent être combinés. Vous pouvez transformer, filtrer et traiter les données de manière modulaire en enchaînant les générateurs. 

Supposons que vous disposiez d'une séquence infinie de nombres et que vous souhaitiez élever chaque nombre au carré et filtrer les résultats impairs :

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1
def square_numbers(sequence):
    for num in sequence:
        yield num ** 2
def filter_evens(sequence):
    for num in sequence:
        if num % 2 == 0:
            yield num

# Compose the generators
numbers = infinite_sequence()
squared = square_numbers(numbers)
evens = filter_evens(squared)

# Print the first 10 even squares
for _ in range(10):
    print(next(evens))
0
4
16
36
64
100
144
196
256
324

Le processus implique que la fonction infinite_sequence génère des nombres indéfiniment, tandis que la fonction square_numbers produit le carré de chaque nombre, puis filter_evens filtre les nombres impairs pour ne produire que des carrés pairs. Notre cursus Associate Python Developer s'intéresse à ce genre de choses, et vous pouvez donc voir comment construire et déboguer des pipelines complexes en utilisant des générateurs, ainsi que des itérateurs et des compréhensions de listes.

Méthodes spéciales pour les générateurs  

Les générateurs sont dotés de méthodes avancées permettant une communication bidirectionnelle et une terminaison contrôlée.

send()

La méthode .send() vous permet de renvoyer des valeurs dans un générateur, le transformant ainsi en coroutine. Ceci est utile pour créer des générateurs interactifs ou avec état.  

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is not None:
            total += value

# Using the generator
acc = accumulator()
next(acc)  # Start the generator
print(acc.send(10))  # Output: 10
print(acc.send(5))   # Output: 15
print(acc.send(20))  # Output: 35

Voici comment cela fonctionne : 

  • Le générateur démarre avec next(acc) pour l'initialiser.  

  • Chaque appel à .send(value) transmet une valeur au générateur, qui est assignée à value dans l'instruction yield.  

  • Le générateur met à jour son état (total) et fournit le nouveau résultat.

jeter()

La méthode .throw() vous permet de lever une exception à l'intérieur du générateur, ce qui peut être utile pour gérer les erreurs ou signaler des conditions spécifiques.  

def resilient_generator():
    try:
        for i in range(5):
            yield i
    except ValueError:
        yield "Error occurred!"

# Using the generator
gen = resilient_generator()
print(next(gen))  # Output: 0
print(next(gen))  # Output: 1
print(gen.throw(ValueError))  # Output: "Error occurred!"

Voici comment cela fonctionne : 

  • Le générateur fonctionne généralement jusqu'à ce que .throw() soit appelé.  

  •  L'exception est levée dans le générateur, qui peut la traiter à l'aide d'un bloc try-except.

close()

La méthode .close() arrête un générateur en levant une exception GeneratorExit. Cette fonction est utile pour nettoyer les ressources ou arrêter les générateurs infinis.  

def infinite_counter():
    count = 0
    try:
        while True:
            yield count
            count += 1
    except GeneratorExit:
        print("Generator closed!")

# Using the generator
counter = infinite_counter()

print(next(counter))  # Output: 0
print(next(counter))  # Output: 1
counter.close()       # Output: "Generator closed!"

Et voici comment cela fonctionne : 

  • Le générateur fonctionne jusqu'à ce que .close() soit appelé.  

  •  L'exception GeneratorExit est levée, ce qui permet au générateur de nettoyer ou d'enregistrer un message avant de se terminer.

Applications concrètes de la science des données  

J'espère que vous commencez à comprendre que les générateurs sont utiles. Dans cette section, je vais essayer de faire ressortir les cas d'utilisation afin que vous puissiez imaginer comment ils fonctionnent réellement dans votre quotidien. 

Traitement de grands ensembles de données  

L'un des défis les plus courants de la science des données est de travailler avec des ensembles de données trop volumineux pour être stockés en mémoire. Les générateurs permettent de traiter ces données ligne par ligne.  

Imaginez que vous disposiez d'un fichier CSV de 10 Go contenant des données de vente et que vous deviez filtrer les enregistrements pour une région spécifique. Voici comment vous pouvez utiliser un pipeline de générateur pour y parvenir :  

import csv
def read_large_csv(file_path):
    """ Generator to read a large CSV file line by line."""
    with open(file_path, mode="r") as file:
        reader = csv.DictReader(file)
        for row in reader:
            yield row

def filter_by_region(data, region):
    """ Generator to filter rows by a specific region."""
    for row in data:
        if row["Region"] == region:
            yield row

# Generator pipeline
file_path = "sales_data.csv"
region = "North America"
data = read_large_csv(file_path)
filtered_data = filter_by_region(data, region)

# Process the filtered data
for record in filtered_data:
    print(record)

Voici ce qui se passe : 

  1. read_large_csv lit le fichier ligne par ligne, en produisant chaque ligne sous forme de dictionnaire.  

  2. filter_by_region filtre les lignes en fonction de la région spécifiée.  

  3. Le pipeline traite les données de manière incrémentale, évitant ainsi la surcharge de la mémoire.

Cette approche profite aux flux de travail d'extraction, de transformation et de chargement, où les données doivent être nettoyées et transformées avant d'être analysées. Vous verrez ce genre de choses dans notre cours ETL et ELT en Python.

Streaming et pipelines  

Les données arrivent parfois en flux continu. Pensez aux données des capteurs, aux flux en direct ou aux médias sociaux.

Supposons que vous travailliez avec des appareils IoT qui génèrent des relevés de température toutes les secondes. Vous souhaitez calculer la température moyenne sur une fenêtre glissante de 10 relevés :

def sensor_data_stream():
    """Simulate an infinite stream of sensor data."""
    import random
    while True:
        yield random.uniform(0, 100)  # Simulate sensor data

def sliding_window_average(stream, window_size):
    """ Calculate the average over a sliding window of readings."""
    window = []
    for value in stream:
        window.append(value)
        if len(window) > window_size:
            window.pop(0)
        if len(window) == window_size:
            yield sum(window) / window_size

# Generator pipeline
sensor_stream = sensor_data_stream()
averages = sliding_window_average(sensor_stream, window_size=10)

# Print the average every second
for avg in averages:
    print(f"Average temperature: {avg:.2f}")

Voici l'explication : 

  1. sensor_data_stream simule un flux infini de lectures de capteurs.  

  2. sliding_window_average maintient une fenêtre glissante des 10 derniers relevés et calcule leur moyenne.  

  3. Le pipeline traite les données en temps réel, ce qui le rend idéal pour la surveillance et l'analyse.

Autres cas d'utilisation

Les générateurs sont également utilisés dans les situations où la taille des données est imprévisible ou lorsqu'elle est infinie.  

Récupération de données sur Internet  

Lorsque vous scrapez des sites web, vous ne savez souvent pas combien de pages ou d'éléments vous devrez traiter. Les générateurs vous permettent de gérer cette imprévisibilité avec élégance :

def scrape_website(url):
    """ Generator to scrape a website page by page."""
    while url:
        # Simulate fetching and parsing a page
        print(f"Scraping {url}")
        data = f"Data from {url}"
        yield data
        url = get_next_page(url)  # Hypothetical function to get the next page

# Usage
scraper = scrape_website("https://example.com/page1")
for data in scraper:
    print(data)

Tâches de simulation

Dans les simulations, telles que les méthodes de Monte Carlo ou le développement de jeux, les générateurs peuvent représenter des séquences infinies ou dynamiques :

def monte_carlo_simulation():
    """ Generator to simulate random events for Monte Carlo analysis."""
    import random
    while True:
        yield random.random()

# Usage
simulation = monte_carlo_simulation()
for _ in range(10):
    print(next(simulation))

Benchmarks sur la mémoire et la vitesse

En raison de leur mode de fonctionnement, les générateurs excellent dans les scénarios où l'efficacité de la mémoire est essentielle, mais (vous serez peut-être surpris de l'apprendre) ils ne constituent pas toujours l'option la plus rapide. Comparons les générateurs et les listes pour comprendre leurs avantages.  

Précédemment, nous avons montré que les générateurs étaient meilleurs que les listes en termes de mémoire. C'est dans cette partie que nous avons comparé l'utilisation de la mémoire lors de la génération d'une séquence de 10 millions de nombres. Procédons maintenant à une autre comparaison, celle de la vitesse :

import time

# List comprehension
start_time = time.time()
sum([x**2 for x in range(1_000_000)])
print(f"List comprehension time: {time.time() - start_time:.4f} seconds")

# Generator expression
start_time = time.time()
sum(x**2 for x in range(1_000_000))
print(f"Generator expression time: {time.time() - start_time:.4f} seconds")
List comprehension time: 0.1234 seconds
Generator expression time: 0.1456 seconds

Bien qu'un générateur permette d'économiser de la mémoire, dans ce cas, il est en fait plus lent que la liste. Cela s'explique par le fait que, pour cet ensemble de données plus petit, la pause et la reprise de l'exécution entraînent des frais généraux.  

La différence de performance est négligeable pour les petits ensembles de données, mais pour les grands ensembles de données, les économies de mémoire des générateurs l'emportent souvent sur la légère pénalité de vitesse.  

Questions qui se posent

Enfin, examinons quelques erreurs ou problèmes courants :

Les générateurs sont épuisables 

Une fois qu'un générateur est épuisé, il ne peut pas être réutilisé. Vous devrez le recréer si vous souhaitez itérer à nouveau.

gen = (x for x in range(5))
print(list(gen))  # Output: [0, 1, 2, 3, 4]
print(list(gen))  # Output: [] (the generator is exhausted)

L'évaluation paresseuse peut être délicate 

Comme les générateurs produisent des valeurs à la demande, des erreurs ou des effets secondaires peuvent n'apparaître que lorsque le générateur est itéré.  

Vous pouvez abuser des générateurs 

Pour les petits ensembles de données ou les tâches simples, les frais généraux liés à l'utilisation d'un générateur peuvent ne pas valoir les économies de mémoire. Prenons l'exemple suivant : je matérialise des données pour plusieurs itérations.

# Generator expression
gen = (x**2 for x in range(10))

# Materialize into a list
squares = list(gen)

# Reuse the list
print(sum(squares))  # Output: 285
print(max(squares))  # Output: 81

Choisir le moment d'utiliser un groupe électrogène

Pour récapituler, je fournirai quelques règles très générales sur le moment où il convient d'utiliser les générateurs. A utiliser pour :

  • Grands ensembles de données: Utilisez les générateurs lorsque vous travaillez avec des ensembles de données trop volumineux pour être stockés en mémoire.  
  • Séquences infinies: Utilisez des générateurs pour représenter des séquences infinies, telles que des flux de données en direct ou des simulations.
  • Pipelines: Utilisez des générateurs pour créer des pipelines de traitement de données modulaires qui transforment et filtrent les données de manière incrémentielle.  

Quand matérialiser les données à la place (convertir en liste)  

  • Petits ensembles de données: N'utilisez pas de générateurs si la mémoire n'est pas un problème et si vous avez besoin d'un accès rapide à tous les éléments ; utilisez plutôt une liste.  
  • Itérations multiples: N'utilisez pas de générateurs si vous devez itérer plusieurs fois sur les mêmes données ; matérialisez-les plutôt dans une liste pour éviter de recréer le générateur.  

Conclusion et points clés à retenir  

Tout au long de cet article, nous avons exploré la manière dont les générateurs peuvent vous aider à relever les défis réels de la science des données, du traitement de grands ensembles de données à la création de pipelines de données en temps réel. Continuez à vous entraîner. La meilleure façon de maîtriser les générateurs est de les utiliser dans votre propre travail. Pour commencer, essayez de remplacer la compréhension d'une liste par une expression génératrice ou de remanier une boucle en une fonction génératrice. 

Une fois que vous aurez maîtrisé les bases, vous pourrez explorer des sujets nouveaux et plus avancés qui s'appuient sur le concept de générateur :  

  • Coroutines: Utilisez .send() et .throw() pour créer des générateurs capables de recevoir et de traiter des données, permettant ainsi une communication bidirectionnelle.  

  • Programmation asynchrone: Combinez les générateurs avec la bibliothèque asyncio de Python pour construire des applications efficaces et non bloquantes.  

  • Concurrence: Découvrez comment les générateurs peuvent mettre en œuvre le multitâche coopératif et la concurrence légère.  

Continuez à apprendre et devenez un expert. Suivez dès aujourd'hui notre cursus professionnel de développeur Python ou notre cursus de compétences en programmation Python. Cliquez sur le lien ci-dessous pour commencer.

Devenez un scientifique ML

Maîtriser Python pour devenir un scientifique de l'apprentissage automatique

Oluseye Jeremiah's photo
Author
Oluseye Jeremiah
LinkedIn

Rédacteur technique spécialisé dans l'IA, la ML et la science des données, rendant les idées complexes claires et accessibles.

FAQ sur les générateurs Python

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

Un générateur Python est un type spécial de fonction qui utilise la fonction yield pour renvoyer un itérateur, produisant des valeurs une par une et conservant la mémoire en ne stockant pas toute la séquence en une seule fois.

Quand dois-je utiliser des générateurs plutôt que des listes de compréhension ?

Les générateurs sont idéaux pour traiter des séquences importantes ou infinies lorsque l'efficacité de la mémoire est essentielle, tandis que les compréhensions de listes fonctionnent bien lorsque vous avez besoin d'une liste complète pour un accès répété ou un indexage aléatoire.

Comment les générateurs améliorent-ils les performances ?

En produisant une valeur à la fois (évaluation paresseuse), les générateurs calculent les valeurs à la volée, ce qui réduit l'utilisation de la mémoire et accélère le traitement par rapport à la construction de structures de données complètes en mémoire.

Puis-je itérer sur un générateur plus d'une fois ?

Non, les générateurs sont épuisés après une itération complète. Pour itérer à nouveau, vous devez créer une nouvelle instance de générateur.

Quels sont les cas pratiques d'utilisation des générateurs ?

Les générateurs sont utiles pour le traitement de grands ensembles de données, la diffusion de données en temps réel, la construction de pipelines efficaces et le traitement de séquences infinies comme celles que l'on trouve dans les tâches algorithmiques et de simulation.

Sujets

Apprenez Python avec DataCamp

Certification disponible

cours

Introduction à Python

4 hr
6M
Maîtrisez les bases de l'analyse de données avec Python en seulement quatre heures. Ce cours en ligne vous présentera l'interface Python et explorera les packages populaires.
Afficher les détailsRight Arrow
Commencer le cours
Voir plusRight Arrow