{}const=>[]async()letfn</>var
DéveloppementPython

Générateurs et itérateurs en Python : économiser de la mémoire et augmenter les performances

Dans cet article, nous expliquons comment utiliser les générateurs et les itérateurs en Python pour travailler efficacement avec de grandes quantités de données. Exemples pratiques de traitement de fichiers, d'API et de pipelines de données. Apprenez à écrire du code qui s'adapte à n'importe quelle quantité d'informations sans dépasser la mémoire.

К

Kodik

Auteur

7 min de lecture

Imaginez que vous devez traiter un fichier journal de plusieurs gigaoctets. La solution la plus simple consiste à charger l'intégralité du fichier en mémoire, à le diviser en lignes et à commencer le traitement. Mais que se passe-t-il s'il n'y a pas assez de RAM ? Ou s'il y a des dizaines de fichiers de ce type ? C'est pour de telles situations que Python a des générateurs et des itérateurs — des outils puissants qui vous permettent de travailler avec de grandes quantités de données sans les charger complètement en mémoire.

Que sont les itérateurs ?

Un itérateur est un objet qui vous permet de parcourir les éléments d'une collection un par un sans charger toute la collection en mémoire à la fois. En Python, tout objet qui implémente la méthode __iter__() et __next__() est un itérateur.

Lorsque vous écrivez for item in collection, Python appelle implicitement iter(collection) pour obtenir un itérateur, puis appelle plusieurs fois next() pour obtenir l'élément suivant jusqu'à ce qu'une exception soit levée StopIteration.

# Exemple d'itérateur simple
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

# Utilisation
counter = CountDown(5)
for num in counter:
    print(num)  # Affichera : 5, 4, 3, 2, 1

🔥 100 000+ étudiants déjà avec nous

Marre de lire la théorie ?
Il est temps de coder !

Kodik — une appli où tu apprends à coder par la pratique. Mentor IA, leçons interactives, projets réels.

🤖 IA 24/7
🎓 Certificats
💰 Gratuit
🚀 Commencer
Ont rejoint aujourd'hui

Générateurs : itérateurs sous stéroïdes

Les générateurs sont un moyen simplifié de créer des itérateurs. Au lieu d'écrire une classe avec les méthodes __iter__() et __next__(), vous créez simplement une fonction avec le mot-clé yield. Le générateur enregistre automatiquement son état entre les appels et reprend l'exécution à l'endroit où il s'est arrêté.

def count_down(start):
    while start > 0:
        yield start
        start -= 1

# Utilisation
for num in count_down(5):
    print(num)  # Affichera : 5, 4, 3, 2, 1

L'avantage est évident : le code est devenu plus compact et plus clair, et la fonctionnalité est restée la même.

Pourquoi cela permet-il d'économiser de la mémoire ?

Le principal avantage des générateurs est le calcul paresseux. Les éléments ne sont créés que lorsqu'ils sont nécessaires et peuvent être immédiatement traités et oubliés. Prenons un exemple concret :

# Mauvais : tout est chargé en mémoire
def read_large_file_bad(file_path):
    with open(file_path, 'r') as file:
        lines = file.readlines()  # Tout le fichier est en mémoire !
    return [line.strip().upper() for line in lines]

# Bien : nous traitons ligne par ligne
def read_large_file_good(file_path):
    with open(file_path, 'r') as file:
        for line in file:  # file est déjà un itérateur !
            yield line.strip().upper()

# Utilisation
for processed_line in read_large_file_good('huge_log.txt'):
    # Nous traitons une ligne à la fois
    process(processed_line)

Dans le premier cas, si le fichier pèse 2 Go, votre programme prendra au moins 2 Go de RAM. Dans le second cas, seulement quelques kilo-octets, quelle que soit la taille du fichier.

Exemples pratiques d'utilisation.

Traitement des données de l'API

Les API renvoient souvent des données page par page. Le générateur peut masquer cette complexité :

def fetch_all_users(api_client, page_size=100):
    page = 1
    while True:
        users = api_client.get_users(page=page, size=page_size)
        if not users:
            break
        for user in users:
            yield user
        page += 1

# Utilisation
for user in fetch_all_users(api):
    print(user['name'])  # Nous traitons les utilisateurs un par un

Séquences infinies

Les générateurs vous permettent de créer des séquences infinies sans risque de débordement de mémoire :

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

# Vous pouvez prendre autant d'éléments que vous le souhaitez
from itertools import islice
first_ten = list(islice(fibonacci(), 10))
print(first_ten)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Convoyeurs de traitement de données

Les générateurs sont parfaits pour créer des chaînes de traitement :

def read_csv(file_path):
    with open(file_path) as f:
        for line in f:
            yield line.strip().split(',')

def filter_by_status(rows, status):
    for row in rows:
        if row[2] == status:  # statut dans la troisième colonne
            yield row

def extract_emails(rows):
    for row in rows:
        yield row[1]  # e-mail dans la deuxième colonne

# Assemblage du convoyeur
rows = read_csv('users.csv')
active_users = filter_by_status(rows, 'active')
emails = extract_emails(active_users)

# C'est seulement ici que le vrai traitement commence
for email in emails:
    send_notification(email)

La beauté de cette approche est que chaque étape du pipeline est indépendante et testable, et la mémoire n'est utilisée que pour un seul élément.

Expressions génératrices

En plus des fonctions génératrices, Python prend en charge les expressions génératrices, une syntaxe compacte pour créer des générateurs :

# List comprehension — crée une liste en mémoire
squares_list = [x**2 for x in range(1000000)]  # Prend beaucoup de mémoire

# Generator expression — crée un générateur
squares_gen = (x**2 for x in range(1000000))  # Ne prend presque pas de mémoire

# Peut être utilisé dans les fonctions
total = sum(x**2 for x in range(1000000))  # Efficace !

Veuillez noter que des parenthèses sont utilisées à la place des crochets. C'est l'un des changements les plus simples et les plus efficaces que vous puissiez apporter à votre code pour économiser de la mémoire.

Quand ne faut-il PAS utiliser de générateurs ?

Malgré tous les avantages, les générateurs ne sont pas toujours adaptés :

Lorsque vous avez besoin d'un accès multiple aux données : le générateur ne peut être passé qu'une seule fois. Si vous avez besoin d'accéder aux données plusieurs fois, il est préférable d'utiliser une liste.

Lorsque la vitesse d'accès est importante : l'élément est obtenu instantanément par index à partir de la liste, et le générateur devra être parcouru depuis le début.

Lorsque les données sont placées en mémoire : si vous avez un petit ensemble de données, les frais généraux de création d'un générateur peuvent être injustifiés.

Quand vous avez besoin de connaître la longueur : len() ne fonctionne pas avec les générateurs, et le calcul de la longueur nécessite une recherche exhaustive.

Techniques avancées.

Délégation de générateurs

Avec l'aide de yield from, vous pouvez déléguer l'exécution à un autre générateur :

def read_multiple_files(*file_paths):
    for path in file_paths:
        with open(path) as f:
            yield from f  # Déléguer la lecture ligne par ligne

# Lire plusieurs fichiers comme un seul flux
for line in read_multiple_files('log1.txt', 'log2.txt', 'log3.txt'):
    process(line)

Communication bidirectionnelle avec les générateurs

Les générateurs peuvent non seulement donner des valeurs par le biais de yield, mais aussi les accepter par la méthode send() :

def running_average():
    total = 0
    count = 0
    average = None
    while True:
        value = yield average
        total += value
        count += 1
        average = total / count

# Utilisation
avg = running_average()
next(avg)  # Démarrage du générateur
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
print(avg.send(30))  # 20.0

Mesurer l'efficacité

Voyons la différence réelle dans l'utilisation de la mémoire :

import sys

# Liste
numbers_list = [x for x in range(1000000)]
print(f"List: {sys.getsizeof(numbers_list) / 1024 / 1024:.2f} MB")

# Générateur
numbers_gen = (x for x in range(1000000))
print(f"Generator: {sys.getsizeof(numbers_gen) / 1024:.2f} KB")

Sur ma machine, le résultat montre une différence de milliers de fois : la liste prend environ 8 Mo et le générateur moins de 0,1 Ko.

Conclusion

Les générateurs et les itérateurs ne sont pas seulement du sucre syntaxique, mais un outil fondamental pour travailler avec de grandes quantités de données. Ils vous permettent d'écrire un code plus efficace qui s'adapte à n'importe quelle taille de données. Comprendre quand et comment utiliser les générateurs distingue un développeur expérimenté d'un débutant.

Commencez simplement : remplacez les compréhensions de liste par des expressions de générateur là où vous n'avez pas besoin d'un accès multiple aux données. Essayez ensuite d'écrire une fonction génératrice pour traiter les fichiers. Et progressivement, vous constaterez que vous pensez aux flux de données, et non aux collections en mémoire.

Annexe Code propose des cours structurés pour les développeurs débutants, couvrant Python, JavaScript, HTML, CSS et de nombreuses autres technologies.

Rejoignez notre Chaîne Telegram, où vous trouverez le soutien de la communauté, des ressources supplémentaires et des réponses aux questions de développeurs expérimentés.

Apprenez à programmer efficacement et avec plaisir !

🎯Arrête de reporter

Tu as aimé l'article ?
Place à la pratique !

Avec Kodik, tu ne lis pas seulement — tu codes immédiatement. Théorie + pratique = vraies compétences.

Pratique instantanée
🧠L'IA explique le code
🏆Certificat

Sans inscription • Sans carte