Générateurs Python et rendement expliqués : Économisez de la mémoire avec des exemples réels
Les générateurs produisent des valeurs une par une au lieu de créer une liste complète en mémoire. Regardez exactement comment rendement met une fonction en pause, voyez la différence de mémoire que nous avons mesurée et créez un petit pipeline de données.
- Une fonction contenant
yieldrenvoie un générateur et son corps ne s’exécute que lorsque vous demandez une valeur. yieldmet la fonction en pause et conserve son état local jusqu’à la prochaine requête.- Les générateurs utilisent une petite quantité fixe de mémoire, quel que soit le nombre de valeurs qu’ils produisent.
- Un générateur ne peut être consommé qu’une seule fois, ce qui est le piège le plus courant.
Imaginez charger un fichier journal de 10 Go dans une liste juste pour trouver les erreurs. Votre programme peut manquer de mémoire avant d’en trouver une. Les générateurs Python résolvent ce problème en produisant des valeurs à la demande, une par une, afin que vous puissiez travailler sur des données volumineuses, voire infinies, avec presque aucune mémoire.
Ce guide montre ce qui se passe réellement lorsqu’un générateur fonctionne, étape par étape. Nous avons exécuté chaque exemple sur Python 3.13 et le résultat provient de cette exécution. Il est utile d’être déjà à l’aise avec les fonctions et les boucles, ce que couvre notre Tutoriel Python pour débutants.
Qu’est-ce qu’un générateur ?
Un générateur est un type spécial d’itérateur que vous créez en écrivant une fonction à l’aide de yield. Toute fonction contenant yield devient automatiquement une fonction génératrice, et son appel vous donne un objet générateur plutôt que d’exécuter le code.
def count_up(limit):
print("starting")
n = 1
while n <= limit:
yield n
n += 1
print("finished")
gen = count_up(2)
print(gen)
# <generator object count_up at 0x...>
Notez que « starting » ne s’est pas imprimé. L’appel de la fonction a uniquement créé le générateur. Rien à l’intérieur n’a encore fonctionné.
Comment fonctionne le rendement, étape par étape
Chaque appel à next() exécute la fonction jusqu’à ce qu’elle atteigne un yield, restitue cette valeur et se fige sur place. L’appel suivant reprend juste après cette ligne, avec chaque variable locale intacte.
print(next(gen))
# starting
# 1
print(next(gen))
# 2
next(gen)
# finished
# StopIteration
Suivez attentivement la sortie. “starting” n’apparaît que sur le premier next(). Au deuxième, l’exécution continue là où elle s’est arrêtée, donc n est toujours disponible et devient 2. Au troisième, la boucle se termine, “terminé” s’imprime, la fonction tombe à la fin et Python lève StopIteration pour signaler qu’il ne reste plus rien.
Vous appelez rarement next() vous-même. Une boucle for le fait pour vous et gère StopIteration tranquillement, tout comme list(), sum() et des fonctions similaires :
print(list(count_up(3)))
# starting
# finished
# [1, 2, 3]
rendement vs rendement
| Aspect | retour |
rendement |
|---|---|---|
| Ce que ça fait | Termine la fonction | Met la fonction en pause |
| Valeurs produites | Un résultat | Une série, une à la fois |
| État local | Rejeté | Conservé entre les valeurs |
| Quand le code s’exécute | Immédiatement lorsqu’on l’appelle | Uniquement lorsqu’une valeur est demandée |
| Mémoire pour de nombreuses valeurs | Doit les contenir tous | Tient un à la fois |
Ce délai est connu sous le nom d’évaluation paresseuse : les valeurs ne sont élaborées qu’en cas de besoin. Un simple return à l’intérieur d’un générateur le termine simplement, ce qui déclenche StopIteration.
La différence de mémoire, mesurée
Nous avons comparé une liste d’un million de carrés avec un générateur équivalent, en utilisant sys.getsizeof :
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen))
# 8448728 200
La propre structure de la liste prenait environ 8,4 Mo, alors que le générateur ne prenait que 200 octets sur notre système. L’écart réel est encore plus grand, car getsizeof ne compte pas les objets entiers dans la liste. La taille du générateur reste également la même pour un milliard d’éléments, puisqu’il stocke uniquement son état actuel et non ses résultats.
Il y a cependant un compromis. Une liste peut être réutilisée, indexée et mesurée avec len(). Un générateur n’offre rien de tout cela. Si vous avez besoin d’un accès aléatoire ou si vous effectuez plusieurs boucles sur une petite quantité de données, une liste est plus simple. Consultez notre guide sur Liste Python vs tuple vs set pour en savoir plus sur ces types.
Expressions génératrices
Pour les cas simples, vous n’avez pas besoin de fonction. Une expression génératrice ressemble à une compréhension de liste avec des parenthèses à la place des carrés :
squares = [n * n for n in range(5)] # builds a list now
squares = (n * n for n in range(5)) # produces values on demand
print(sum(n * n for n in range(5)))
# 30
Lorsqu’une expression génératrice est le seul argument d’une fonction, comme dans sum(), vous pouvez laisser de côté les crochets supplémentaires. Il s’agit d’une bonne habitude pour les totaux sur des données volumineuses, car aucune liste temporaire n’est créée.
Construire un pipeline de données
C’est là que les générateurs brillent vraiment. Les petits générateurs peuvent être enchaînés afin que chaque élément passe par chaque étape, un à la fois :
def read_lines(lines):
for line in lines:
yield line.strip()
def only_errors(lines):
for line in lines:
if line.startswith("ERROR"):
yield line
def messages(lines):
for line in lines:
yield line.split(" ", 1)[1]
log = ["INFO started\n", "ERROR db down\n",
"ERROR disk full\n", "INFO stopped\n"]
print(list(messages(only_errors(read_lines(log)))))
# ['db down', 'disk full']
Chaque étape effectue un petit travail et peut être testée individuellement. En utilisation réelle, remplacez la liste d’exemples par un fichier ouvert, car les objets fichier peuvent être lus ligne par ligne, comme le montre notre guide sur la gestion des fichiers Python. Même un journal de plusieurs gigaoctets serait alors transmis avec très peu de mémoire.
Astuce : Un pipeline ne fonctionne pas jusqu’à ce que quelque chose le consomme. La construction de la chaîne est instantanée. Le traitement n’a lieu que lorsque vous effectuez une boucle sur le générateur final ou que vous l’encapsulez dans un appel comme list().
Séquences infinies
Une liste ne peut pas être infinie, mais un générateur le peut, car il ne calcule que la valeur suivante. Utilisez itertools.islice pour en prendre autant que nécessaire :
import itertools
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(itertools.islice(fibonacci(), 8)))
# [0, 1, 1, 2, 3, 5, 8, 13]
Attention cependant : appeler list(fibonacci()) sans limite s’exécuterait jusqu’à ce que votre mémoire soit épuisée. Le même modèle convient aux générateurs d’ID, aux boucles d’interrogation et aux résultats d’API paginés, où vous récupérez la page suivante uniquement lorsque la page actuelle est épuisée.
rendement et lots
yield from passez à un autre itérable, vous enregistrant une boucle imbriquée :
def chain_all(*groups):
for group in groups:
yield from group
print(list(chain_all([1, 2], (3, 4))))
# [1, 2, 3, 4]
Un modèle très pratique consiste à diviser les données en lots de taille fixe, par exemple pour insérer des milliers de lignes à la fois dans une base de données :
def batches(items, size):
batch = []
for item in items:
batch.append(item)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batch
print(list(batches(range(7), 3)))
# [[0, 1, 2], [3, 4, 5], [6]]
Le dernier if batch garantit qu’un groupe restant plus petit n’est pas perdu. Python 3.12 et versions ultérieures incluent également itertools.batched, qui fait un travail similaire.
envoyer, fermer et nettoyer
Les générateurs peuvent également recevoir des valeurs. send() reprend le générateur et la valeur devient le résultat de l’expression yield. Vous devez d’abord appeler next() une fois pour atteindre le premier yield :
def running_total():
total = 0
while True:
value = yield total
total += value
t = running_total()
next(t)
print(t.send(10), t.send(5))
# 10 15
La plupart des gens en ont rarement besoin. Une préoccupation plus courante est le nettoyage. Si vous vous arrêtez tôt, un bloc finally s’exécute toujours lorsque le générateur est fermé :
def tracked():
try:
yield 1
yield 2
finally:
print("cleanup ran")
tr = tracked()
next(tr)
tr.close()
# cleanup ran
Pytest utilise cette même idée : un appareil avec yield exécute ensuite son nettoyage, comme le démontre notre tutoriel pytest. De même, le décorateur contextmanager de la bibliothèque standard transforme un générateur en bloc with.
Générateurs en classe
Écrire une méthode __iter__ en tant que générateur est le moyen le plus simple de rendre votre propre classe itérable. Un avantage est que chaque boucle reçoit un nouveau générateur, de sorte que l’objet peut être bouclé encore et encore :
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
c = Countdown(3)
print(list(c), list(c))
# [3, 2, 1] [3, 2, 1]
Quand utiliser un générateur
| Utilisez un générateur lorsque | Utilisez une liste lorsque |
|---|---|
| Les données sont volumineuses ou leur taille est inconnue | Les données sont petites et tiennent en mémoire |
| Vous traitez les articles une seule fois, dans l’ordre | Tu fais plusieurs boucles |
| Vous pouvez arrêter plus tôt | Vous avez besoin de len(), d’indexation ou de découpage |
| Vous lisez un flux ou des fichiers | Vous devez trier ou inverser les données |
| Vous construisez un pipeline | La simplicité compte plus que la mémoire |
Un générateur n’est pas toujours globalement plus rapide. Si vous finissez par utiliser chaque valeur, le travail total est similaire et la compréhension d’une liste peut même être plus rapide pour les petites données, car elle entraîne moins de frais généraux par élément. Les principaux avantages sont l’utilisation de la mémoire et l’obtention des premiers résultats plus tôt.
Erreurs courantes
Réutiliser un générateur épuisé
Cela attrape presque tout le monde à un moment donné. Un générateur se souvient qu’il est terminé :
g = count_up(2)
print(list(g), list(g))
# [1, 2] []
Le deuxième appel obtient une liste vide sans aucune erreur. Appelez à nouveau la fonction pour obtenir un nouveau générateur ou utilisez l’approche de classe présentée ci-dessus.
Attendre que le code s’exécute immédiatement
def noisy():
print("body runs")
yield 1
n = noisy() # nothing is printed
print(next(n)) # body runs, then 1
Les vérifications d’arguments en haut d’un générateur ne s’exécutent qu’au premier next(), donc les erreurs peuvent apparaître loin de l’appel qui les a provoquées.
Autres pièges
- Appel de
len()ou indexation. Les générateurs ne prennent en charge ni l’un ni l’autre et génèrent unTypeError. - Fermer un fichier trop tôt. Renvoyer un générateur depuis l’intérieur d’un bloc
withpeut signifier que le fichier se ferme avant qu’une ligne ne soit lue. Mettez leyieldà l’intérieur du blocwith. - Tout envelopper dans
list(). Cela annule l’avantage de la mémoire. - En utiliser un pour les données minuscules. Une liste simple est souvent plus claire.
Pour conclure
Un générateur est une fonction qui peut faire une pause. Cette idée vous offre une faible utilisation de la mémoire, des séquences infinies et des pipelines propres. Pour le mettre en pratique, trouvez un endroit dans votre code qui crée une grande liste uniquement pour la parcourir une seule fois, et transformez-la en générateur. Essayez ensuite de chaîner deux petits générateurs sur un vrai fichier journal.
Questions fréquentes
Que fait le rendement en Python ?
rendement produit une valeur et met la fonction en pause, en conservant ses variables locales. La fonction reprend au même point lorsque la valeur suivante est demandée.
Quelle est la différence entre un générateur et une liste ?
Une liste stocke toutes ses valeurs en mémoire à la fois. Un générateur calcule chaque valeur à la demande, utilise peu de mémoire et ne peut généralement être consommé qu’une seule fois.
Quelle est la différence entre un itérateur et un générateur ?
Chaque générateur est un itérateur. Un générateur est tout simplement le moyen le plus simple d’en écrire un, car Python crée les méthodes __iter__ et __next__ pour vous.
Pouvez-vous boucler deux fois sur un générateur ?
Non. Une fois épuisé, il ne donne plus rien, sans générer d’erreur. Créez un nouveau générateur en appelant à nouveau la fonction ou stockez les résultats dans une liste.
Les générateurs sont-ils plus rapides que les listes ?
Pas toujours. Ils utilisent beaucoup moins de mémoire et fournissent le premier résultat plus tôt, mais la compréhension d’une liste peut être légèrement plus rapide lorsque vous consommez entièrement de petites quantités de données.
Continuez à apprendre
Besoin d’aide pour les tests, WordPress ou le référencement ?
Nous aidons les équipes à proposer de meilleurs logiciels et à développer leurs sites. Dites-nous sur quoi vous travaillez et nous vous proposerons une prochaine étape claire.
Plus de guides sur tests, IA et développement moderne
Vous avez apprécié cet article ? Choisissez un sujet et accédez directement à nos nouveaux didacticiels.
Les hallucinations de l'IA expliquées : pourquoi les modèles de langage inventent les choses et comment les réduire
Les hallucinations de l'IA ont expliqué : ce qu'elles sont, pourquoi les modèles de langage les produisent, les types courants, comment les réduire …
Lire l'article →DevOpsVariables d'environnement expliquées : fichiers .env, secrets et configuration correctement effectués
Variables d'environnement expliquées avec des exemples Python que nous avons exécutés : lecture et définition, pourquoi chaque valeur est une chaîne…
Lire l'article →Développement logicielLimitation du débit de l'API expliquée : algorithmes, en-têtes et gestion des erreurs 429
Limitation du débit de l'API expliquée avec une simulation que nous avons exécutée : compartiment de jetons, fenêtre fixe, fenêtre glissante, 429 ré…
Lire l'article →Automatisation des testsSe moquer en Python : unittest.mock, patch et les erreurs qui font mentir les tests
Moquerie en Python avec unittest.mock, testée : Mock et return_value, side_effect, correctifs où un nom est utilisé, spec et autospec, dates moqueus…
Lire l'article →Tests logicielsCouverture du code expliquée : couverture des lignes par rapport aux branches et ce que signifie réellement 100 %
Couverture du code expliquée avec une véritable exécution de cover.py : couverture des lignes, des branches et des conditions, lecture d'un rapport,…
Lire l'article →Bases de données et SQLDELETE vs TRUNCATE vs DROP dans SQL : différences, restauration et quand les utiliser
DELETE vs TRUNCATE vs DROP expliqué avec SQL testé : ce que chacun supprime, restauration, déclencheurs, réinitialisation d'identité, vitesse et en …
Lire l'article →Bases de données et SQLVues SQL expliquées : créez, utilisez et évitez les pièges, avec des exemples
Vues SQL expliquées avec les requêtes que nous avons exécutées : création d'une vue, résultats en direct, masquage des colonnes, performances, vues …
Lire l'article →Développement logicielInterface TypeScript vs Type : différences réelles, testées avec le compilateur
Interface TypeScript vs type, vérifiée avec le vrai compilateur : ce que les deux peuvent faire, ce qu'un seul peut faire, la fusion des déclaration…
Lire l'article →Développement logicielPython *args et **kwargs expliqués : fonctions flexibles avec des exemples clairs
Python *args et **kwargs expliqués avec le code que nous avons exécuté : emballage et déballage, ordre des paramètres, arguments de mot-clé uniqueme…
Lire l'article →Développement logicielFonctions Python Lambda : syntaxe, utilisations réelles et quand ne pas les utiliser
Fonctions Python lambda expliquées avec le code que nous avons exécuté : syntaxe, tri avec clé, carte et filtre, fermetures, piège de liaison tardiv…
Lire l'article →Intelligence artificielleQue sont les agents IA ? Comment ils fonctionnent, avec une simple boucle d'agent que vous pouvez exécuter
Que sont les agents IA ? Un guide simple sur la boucle d'agent, les outils, la mémoire et les garde-fous, avec une petite boucle Python que nous avo…
Lire l'article →DevOpsSurveillance vs observabilité : la vraie différence, avec les journaux, les métriques et les traces
Surveillance et observabilité expliquées : échecs connus et inconnus, journaux, métriques et traces, présentation pas à pas des incidents de paiemen…
Lire l'article →DevOpsVolumes Docker vs montages Bind : conserver les données du conteneur de la bonne manière
Volumes Docker et montages de liaison expliqués : pourquoi les données du conteneur disparaissent, volumes nommés, montages de liaison, tmpfs, exemp…
Lire l'article →Automatisation des testsTDD vs BDD : principales différences, une fonctionnalité conçue dans les deux sens et quand les utiliser
TDD vs BDD expliqué avec une fonctionnalité de remise construite dans les deux sens : refactor rouge-vert avec le code que nous avons exécuté, scéna…
Lire l'article →Tests logicielsTests d'acceptation utilisateur (UAT) : processus, liste de contrôle et exemples réels
Les tests d'acceptation des utilisateurs sont expliqués : qui les exécute, en quoi ils diffèrent du contrôle qualité, un processus en sept étapes, u…
Lire l'article →Bases de données et SQLExplication des valeurs SQL NULL : IS NULL, COALESCE, NULLIF et les pièges à éviter
Les valeurs SQL NULL expliquées avec les requêtes que nous avons exécutées : logique à trois valeurs, IS NULL, COALESCE, NULLIF, comment les agrégat…
Lire l'article →Bases de données et SQLInstruction SQL CASE WHEN : syntaxe, exemples réels et erreurs courantes
SQL CASE WHEN expliqué avec les requêtes que nous avons exécutées : formulaires simples et recherchés, ordre des conditions, pivots avec SUM, astuce…
Lire l'article →Développement logicielQu'est-ce que JWT ? Les jetons Web JSON expliqués avec un exemple fonctionnel
Qu’est-ce que JWT ? JSON Web Tokens expliqué : les trois parties, un jeton construit et falsifié dans le code que nous avons exécuté, JWT vs session…
Lire l'article →Développement logicielBoucle d'événements JavaScript expliquée : pile d'appels, microtâches et minuteries avec des exemples
La boucle d'événements JavaScript expliquée avec le code que nous avons exécuté : pile d'appels, files d'attente de tâches et de microtâches, pourqu…
Lire l'article →Développement logicielGénérateurs Python et rendement expliqués : économisez de la mémoire avec des exemples réels
Les générateurs Python et le rendement expliqués avec le code que nous avons exécuté : comment ils s'arrêtent et reprennent, les économies de mémoir…
Lire l'article →Intelligence artificielleQue sont les intégrations ? Vecteurs et similarité cosinus expliqués simplement
Que sont les intégrations ? Un guide en langage simple sur les vecteurs, la similarité cosinus et la recherche sémantique, avec un petit exemple que…
Lire l'article →DevOpsDéploiement Bleu-Vert vs Canary : différences, compromis et comment choisir
Explication du déploiement bleu-vert ou canari : comment chacun fonctionne, coût, risque et restauration, regroupement d'utilisateurs stable que nou…
Lire l'article →Automatisation des testsTests instables : quelles sont leurs causes et comment les corriger définitivement
Des tests irréguliers expliqués avec des démos que nous avons exécutées : les principales causes, comment en confirmer et en diagnostiquer une, les …
Lire l'article →Tests logicielsTests de charge vs tests de contrainte : différences, métriques et exemples k6
Explication des tests de charge et des tests de stress : ce que chacun prouve, les tests de pointe et de trempage, les métriques qui comptent, pourq…
Lire l'article →Tests logicielsScénario de test et scénario de test : principales différences, exemples et quand les utiliser
Scénario de test et scénario de test expliqués : des définitions claires, un tableau côte à côte, une fonctionnalité de réinitialisation de mot de p…
Lire l'article →Bases de données et SQLOptimisation des requêtes SQL : lire les plans d'exécution et corriger les requêtes lentes, avec des délais
Optimisation des requêtes SQL testée sur un million de lignes : lire les plans d'exécution, ajouter les bons index, corriger les filtres non Sargabl…
Lire l'article →Bases de données et SQLSQL UNION vs UNION ALL : différences, exemples et performances testées
SQL UNION vs UNION ALL expliqué avec les requêtes que nous avons exécutées : des doublons, les règles suivent toutes les deux, un véritable bug de d…
Lire l'article →Développement logicielTutoriel API JavaScript Fetch : GET, POST, gestion des erreurs et délais d'attente
Un didacticiel de l'API JavaScript Fetch testé sur un serveur local : GET et POST, pourquoi 404 ne lance pas, un assistant réutilisable, des chaînes…
Lire l'article →Développement logicielGestion des fichiers Python : lecture, écriture, CSV, JSON et pathlib avec exemples
Gestion des fichiers Python expliquée avec le code que nous avons exécuté : modes ouverts, avec lecture de fichiers volumineux, CSV, JSON, pathlib, …
Lire l'article →Développement logicielClasses et objets Python : didacticiel POO avec des exemples réels
Classes et objets Python expliqués avec le code que nous avons exécuté : __init__, méthodes, attributs de classe par rapport à l'instance, héritage,…
Lire l'article →Intelligence artificielleQu'est-ce qu'un grand modèle de langage ? Comment fonctionnent les LLM, expliqués sans battage médiatique
Qu’est-ce qu’un grand modèle de langage ? Les LLM expliqués en langage simple : jetons, intégrations, attention, formation, fenêtres de température …
Lire l'article →DevOpsCommandes Linux pour les débutants : le guide du terminal essentiel avec des exemples
Commandes Linux pour débutants, regroupées par tâche : navigation, fichiers, affichage des journaux, grep, tuyaux, autorisations, processus et archi…
Lire l'article →Automatisation des testsSelenium vs Playwright : principales différences, code comparé et lequel choisir
Comparatif Selenium vs Playwright : architecture, attente, support du navigateur et du langage, débogage, même test dans les deux outils, conseils d…
Lire l'article →Tests logicielsPartitionnement d'équivalence : technique de conception de tests expliquée avec des exemples concrets
Le partitionnement d'équivalence expliqué : comment trouver des partitions valides et invalides, des exemples concrets d'âges, de mots de passe et d…
Lire l'article →Tests logicielsGravité vs priorité dans les tests logiciels : différences, matrice et exemples réels
Explication de la gravité et de la priorité dans les tests logiciels : des définitions claires, une matrice à quatre voies avec des exemples, des éc…
Lire l'article →Bases de données et SQLTutoriel sur les sous-requêtes SQL : types, sous-requêtes corrélées et EXISTE avec des exemples
Un didacticiel sur les sous-requêtes SQL avec les requêtes que nous avons exécutées : les sous-requêtes scalaires, IN, FROM et SELECT, les sous-requ…
Lire l'article →Bases de données et SQLTransactions SQL et ACID expliquées : COMMIT, ROLLBACK et niveaux d'isolement
Transactions SQL et ACID expliquées avec un virement bancaire que nous avons effectué : COMMIT, ROLLBACK, points de sauvegarde, verrous, niveaux d'i…
Lire l'article →Développement logicielGit merge vs rebase : différences, exemples et quand utiliser chacun
Git merge vs rebase expliqué avec de vraies sorties de log, un tableau comparatif, la règle d'or du rebase et un flux de travail sûr pour les équipes.
Lire l'article →Développement logicielList comprehension Python : syntaxe, exemples et cas à éviter
La list comprehension Python expliquée : syntaxe, filtres, if-else, boucles imbriquées, temps mesurés et cas où une boucle for classique est préféra…
Lire l'article →Bases de données et SQLJointures SQL expliquées avec exemples (Inner, Left, Right, Full)
Comprenez les jointures SQL pas à pas avec des tables et requêtes d'exemple : INNER, LEFT, RIGHT, FULL, CROSS et SELF, plus erreurs courantes et con…
Lire l'article →