Générateurs Python : yield, genexpr et itertools

Découvrez ce qu’est vraiment un générateur et quand y recourir : le protocole d’itération, yield vs return, les expressions génératrices, itertools, et l’enchaînement de petits générateurs en un pipeline paresseux qui streame un gros fichier avec presque aucune mémoire.

Comprenez et utilisez les générateurs Python : le protocole d’itération (iter/next), yield vs return, les expressions génératrices vs les listes, les incontournables d’itertools (islice, chain, groupby, accumulate), l’enchaînement de petits générateurs en un pipeline de données paresseux qui streame un gros fichier une ligne à la fois, le gain de mémoire concret par rapport à une liste, et une brève note sur les générateurs asynchrones.

Date de publication

17 juillet 2026

Modifié

18 juillet 2026

AstucePoints clés
  • Un générateur produit ses valeurs une à une, à la demande — il ne les détient jamais toutes en même temps. C’est tout l’intérêt : vous pouvez parcourir un milliard d’éléments, ou un flux sans fin, dans quelques centaines d’octets, parce que seule la valeur courante est vivante.
  • yield transforme une fonction en générateur. Contrairement à return, yield met en pause la fonction et se souvient de l’endroit où il en était — la valeur suivante reprend juste après le yield, les variables locales intactes. Un générateur est à usage unique : une fois parcouru jusqu’au bout, il est épuisé (recréez-le pour l’itérer à nouveau).
  • Une expression génératrice est une compréhension de liste paresseuse. Remplacez les [...] par des (...)(x*x for x in data) — et rien ne se calcule tant que vous n’itérez pas. Un million de carrés stockés dans une liste pèsent ~40 Mo ; les mêmes valeurs sous forme de générateur sont une recette compacte de 200 octets.
  • itertools est la boîte à outils standard pour les itérateurs. islice (découper sans matérialiser), chain (concaténer des flux), groupby (regrouper les clés égales consécutives), accumulate (totaux cumulés), et les infinis count/cycle/repeat — tous paresseux, tous composables.
  • Enchaînez de petits générateurs en un pipeline de données. lire → filtrer → parser → résumer, chacun un minuscule générateur, pour que les lignes s’écoulent une à la fois — la façon honnête de traiter un fichier trop gros pour être chargé. yield from délègue à un sous-générateur en une ligne.

Introduction

Certaines données ne tiennent pas en mémoire, et certaines ne finissent jamais. Un fichier de logs de 40 Go, un flux de capteurs en direct, chaque ligne renvoyée par un curseur de base de données, une séquence infinie d’identifiants — vous ne pouvez pas les charger d’abord dans une liste puis travailler dessus. Vous devez les traiter un élément à la fois, en tirant la valeur suivante seulement quand vous êtes prêt à la traiter et en laissant filer la précédente. C’est exactement ce que fait un générateur, et yield est le mot-clé qui en crée un.

Cette leçon est le tableau pratique, dans l’ordre où il est utile. D’abord le protocole d’itération — la machinerie iter()/next() sur laquelle tourne déjà chaque boucle for, pour que vous compreniez ce qu’un générateur est. Ensuite yield : comment une fonction génératrice se met en pause et reprend, et pourquoi cela lui permet de conserver son état sans tout stocker. Puis les expressions génératrices — la cousine paresseuse de la compréhension de liste — avec le gain de mémoire montré en chiffres réels. Puis les incontournables d’itertools que vous dégainerez chaque semaine. Puis la récompense : enchaîner de petits générateurs en un pipeline paresseux qui streame un vrai fichier, une ligne à la fois. Nous terminons par le compromis mémoire/vitesse concret et un bref aperçu des générateurs asynchrones.

Les exemples sont volontairement petits et autonomes — des intervalles, des flux, quelques enregistrements — car c’est ainsi que l’on comprend le mieux les générateurs. Pour la section « traiter un fichier sans tout charger », nous streamons le fichier penguins.csv fourni (344 lignes de mesures des manchots Palmer) ligne par ligne, ce qui est le même idiome que vous emploieriez sur un fichier mille fois plus gros. Il n’y a rien à télécharger et aucun accès réseau au rendu. Cela s’appuie sur Compréhensions Python — une expression génératrice n’est qu’à un changement de syntaxe d’une compréhension de liste — et ouvre la série Python Intermédiaire. Chaque résultat ci-dessous a été produit par le code affiché, et les blocs se construisent dans l’ordre. Pour expérimenter, modifiez la cellule Essayez en direct vers la fin et appuyez sur Run.

Le protocole d’itération : ce que fait vraiment une boucle for

Avant les générateurs, comprenez la chose qu’ils sont. Python trace une ligne entre un itérable (quelque chose que vous pouvez parcourir — une liste, une chaîne, un fichier) et un itérateur (l’objet qui produit réellement les valeurs, une à la fois, et se souvient de sa position). Une boucle for est du sucre syntaxique par-dessus deux fonctions natives : iter() obtient un itérateur à partir de l’itérable, et next() tire la valeur suivante jusqu’à ce qu’il n’en reste plus.

nums = [10, 20, 30]
it = iter(nums)          # get an iterator from the list

print(next(it))          # pull values one at a time
print(next(it))
print("is it its own iterator?", iter(it) is it)
10
20
is it its own iterator? True

iter(nums) a renvoyé un itérateur ; chaque next(it) a produit la valeur suivante — 10, puis 20. La dernière ligne affiche True : un itérateur est son propre itérateur, ce qui est la règle qui vous permet de le glisser directement dans une boucle for. Quand next() dépasse la fin, il lève StopIteration, et cette exception est précisément la façon dont une boucle for sait s’arrêter — vous ne la voyez jamais parce que la boucle l’attrape pour vous.

Vous pouvez construire un itérateur à la main en implémentant le protocole — __iter__ (renvoie l’itérateur) et __next__ (produit la valeur suivante ou lève StopIteration). Voici un compte à rebours écrit à la manière longue :

class Countdown:
    def __init__(self, start):
        self.n = start

    def __iter__(self):
        return self                 # I am my own iterator

    def __next__(self):
        if self.n <= 0:
            raise StopIteration     # signal "no more values"
        value = self.n
        self.n -= 1
        return value

print(list(Countdown(3)))
[3, 2, 1]

list() a piloté le protocole pour nous — en appelant __next__ jusqu’à StopIteration — et a collecté [3, 2, 1]. Ça marche, mais voyez tout le cérémonial que cela a demandé : une classe, deux méthodes dunder, un état manuel (self.n) et un StopIteration explicite. C’est ce code répétitif que yield efface.

yield : une fonction génératrice

Placez le mot yield n’importe où dans une fonction et elle cesse d’être une fonction ordinaire : l’appeler renvoie désormais un générateur — un itérateur qui exécute le corps de votre fonction paresseusement. Là où return renvoie une seule valeur et met fin à la fonction pour de bon, yield renvoie une valeur et met en pause, figeant chaque variable locale exactement là où elle est ; le next() suivant reprend sur la ligne juste après le yield. C’est cette pause-reprise qui permet à un générateur de conserver son état d’une valeur à l’autre sans les stocker.

Toute la classe Countdown se réduit à trois lignes :

def countdown(n):
    while n > 0:
        yield n                     # hand back n, then pause here
        n -= 1                      # resumes here on the next call

print(list(countdown(3)))
[3, 2, 1]

Même résultat — [3, 2, 1] — sans classe, sans __next__, sans StopIteration (sortir de la fin de la fonction le lève automatiquement). Le n local survit d’un yield à l’autre parce que la fonction est en pause, pas redémarrée : elle produit 3, met en pause ; reprend, n devient 2, produit 2 ; et ainsi de suite jusqu’à ce que la condition while échoue.

Une propriété surprend tout le monde une fois : un générateur est à usage unique. Il produit ses valeurs exactement une fois, puis il est épuisé — il n’y a rien à rembobiner.

g = countdown(3)
print("first pass: ", list(g))
print("second pass:", list(g))
first pass:  [3, 2, 1]
second pass: []

Le premier passage l’a vidé en [3, 2, 1] ; le second passage a obtenu [], parce que le générateur était déjà épuisé. C’est le piège numéro un des générateurs (nous y revenons dans Problèmes fréquents) : si vous avez besoin des valeurs deux fois, soit recréez le générateur (countdown(3) de nouveau), soit matérialisez-les une fois dans une liste. L’avantage d’être à usage unique, c’est justement le gain de mémoire — un générateur n’a jamais à se souvenir de ce qu’il a déjà produit.

Les expressions génératrices : une compréhension paresseuse

Vous n’avez pas besoin d’une fonction complète pour les cas simples. Une expression génératrice est une compréhension de liste avec des parenthèses rondes au lieu de crochets — et ce seul changement la rend paresseuse : elle ne calcule rien d’avance, produisant chaque valeur seulement quand vous la demandez.

squares_gen = (x * x for x in range(1_000_000))   # note: (...) not [...]

print("type:", type(squares_gen).__name__)
print("first two:", next(squares_gen), next(squares_gen))
type: generator
first two: 0 1

Malgré le fait de nommer un million de carrés, l’objet n’est qu’un générateur — le construire a été instantané et gratuit, parce qu’aucun des carrés n’a encore été calculé. Ce n’est que lorsque nous appelons next() qu’il calcule la première valeur (0), puis la suivante (1). Une compréhension de liste [x*x for x in range(1_000_000)] aurait calculé et stocké le million tout de suite ; le générateur détient une recette, pas les résultats. La différence de taille n’est pas subtile :

import sys

gen = (x * x for x in range(1_000_000))
lst = [x * x for x in range(1_000_000)]

print("generator object:", sys.getsizeof(gen), "bytes")
print("list object:     ", sys.getsizeof(lst), "bytes")
generator object: 200 bytes
list object:      8448728 bytes

Le générateur fait 200 octets quel que soit le nombre de valeurs qu’il produira à terme — c’est une recette de taille fixe. La liste fait 8 448 728 octets (~8 Mo) : un million de références d’entiers, toutes détenues en même temps. Quand vous n’avez besoin que de consommer les valeurs dans l’ordre (les sommer, les filtrer, les envoyer quelque part), le générateur donne la même réponse pour l’équivalent d’une erreur d’arrondi de mémoire.

Et quand une expression génératrice est le seul argument d’une fonction, vous pouvez laisser tomber les parenthèses supplémentaires — cela se lit comme une phrase toute simple :

total = sum(x for x in range(100_000))
print(total)
4999950000

sum(x for x in range(100_000)) envoie les nombres directement dans sum sans jamais construire la liste des 100 000 — le total courant est la seule chose en mémoire — et affiche 4999950000.

Laquelle, et quand ? Utilisez une compréhension de liste quand vous avez besoin des résultats plus d’une fois, que vous devez les indexer ou les découper (result[5]), ou que vous avez besoin de len(). Utilisez une expression génératrice quand vous parcourrez les valeurs exactement une fois et voulez éviter de toutes les détenir — surtout quand elles sont nombreuses, ou que vous les passez directement à sum/min/max/any/all/"".join.

itertools : la boîte à outils des itérateurs

Le module de bibliothèque standard itertools est un ensemble de briques rapides et paresseuses pour les itérateurs. Une poignée revient constamment :

Outil Ce qu’il fait Un usage en une ligne
islice(it, n) Prendre les n premiers (ou une tranche) sans matérialiser islice(huge_stream, 10) — jeter un œil aux 10 premières lignes
chain(a, b, …) Concaténer plusieurs itérables en un seul flux chain(file1, file2) — traiter plusieurs fichiers comme un seul
groupby(it) Regrouper les clés égales consécutives en séries Réduire un journal trié en séries par statut
accumulate(it) Émettre un total courant (ou toute réduction binaire) Une colonne de somme cumulée
count(start, step) Un compteur infini Générer des identifiants 1, 2, 3, …
cycle(it) Répéter un itérable à l’infini Alterner des couleurs en boucle sur des séries de graphique
repeat(x, n) La même valeur n fois (ou à l’infini) Une colonne constante de longueur n
tee(it, n) Séparer un itérateur en n itérateurs indépendants Lire un flux deux fois sans relire la source

Quatre d’entre eux, exécutés ensemble :

import itertools

# islice a slice out of an INFINITE counter — no list is ever built
print("islice:    ", list(itertools.islice(itertools.count(10, 5), 4)))
print("chain:     ", list(itertools.chain([1, 2], [3, 4])))
print("accumulate:", list(itertools.accumulate([1, 2, 3, 4])))
print("groupby:   ", [(k, list(g)) for k, g in itertools.groupby("aabbbc")])
islice:     [10, 15, 20, 25]
chain:      [1, 2, 3, 4]
accumulate: [1, 3, 6, 10]
groupby:    [('a', ['a', 'a']), ('b', ['b', 'b', 'b']), ('c', ['c'])]

Lisez les sorties :

  • islice a tiré les quatre premières valeurs de count(10, 5) — un compteur infini démarrant à 10, avançant par pas de 5 — donnant [10, 15, 20, 25] puis s’arrêtant. islice est la façon de prendre une bouchée finie d’une source sans fin (ou énorme) sans jamais la matérialiser.
  • chain a collé [1, 2] et [3, 4] en un seul flux — [1, 2, 3, 4] — paresseusement, de sorte que concaténer deux fichiers de 10 Go ne coûte rien tant que vous ne lisez pas.
  • accumulate a transformé [1, 2, 3, 4] en ses totaux courants — [1, 3, 6, 10] (1, 1+2, 1+2+3, 1+2+3+4).
  • groupby a parcouru "aabbbc" et regroupé les caractères égaux consécutifs en séries — [(‘a’, [‘a’, ‘a’]), (‘b’, [‘b’, ‘b’, ‘b’]), (‘c’, [‘c’])]. Le mot consécutifs est le piège : groupby ne regroupe que les clés égales adjacentes, donc vous triez presque toujours par la clé d’abord (contrairement au GROUP BY de SQL, qui trie pour vous).

Les infinis — count, cycle, repeat — doivent toujours être bornés par quelque chose comme islice ou un break, sinon ils tourneront indéfiniment. C’est une fonctionnalité : un générateur peut représenter une séquence sans fin précisément parce qu’il n’essaie jamais de la construire.

Les pipelines de données paresseux

C’est ici que les générateurs gagnent leur place. Le vrai travail sur les données est un pipeline : lire une source, parser chaque enregistrement, filtrer ceux qui vous intéressent, puis résumer. Si vous écrivez chaque étape comme un petit générateur, les étapes se composent, et les données s’écoulent à travers toute la chaîne un enregistrement à la fois — la source n’est jamais entièrement en mémoire. C’est ainsi que vous traitez un fichier bien plus gros que votre RAM.

Nous répondrons à une question sur les manchots — quelle est la masse corporelle moyenne des manchots Gentoo ? — en streamant le CSV au lieu de le charger. Trois minuscules générateurs, chacun faisant un seul travail :

import csv

def read_rows(path):
    """Yield one dict per CSV row — the file stays open, one row in memory."""
    with open(path, newline="") as f:
        yield from csv.DictReader(f)      # delegate to the reader's iterator

def only_species(rows, name):
    """Pass through just the rows for one species."""
    for row in rows:
        if row["species"] == name:
            yield row

def field_as_float(rows, field):
    """Pull one numeric field out of each row, skipping blanks."""
    for row in rows:
        value = row[field]
        if value:                         # skip missing/blank cells
            yield float(value)

Chaque fonction prend un itérable et produit un itérable, si bien qu’elles s’emboîtent comme des sections de tuyau. Remarquez yield from dans read_rows : il délègue à l’itérateur propre de csv.DictReader, produisant chacune de ses lignes à tour de rôle — une ligne au lieu d’une boucle for row in reader: yield row. Maintenant, connectez-les et consommez le flux :

rows    = read_rows("penguins.csv")
gentoos = only_species(rows, "Gentoo")
masses  = field_as_float(gentoos, "body_mass_g")

count = 0
total = 0.0
for m in masses:            # nothing has been read yet — this loop pulls it
    count += 1
    total += m

print("Gentoo penguins:", count)
print("mean body mass: ", round(total / count, 1), "g")
Gentoo penguins: 123
mean body mass:  5076.0 g

123 manchots Gentoo avec une masse corporelle enregistrée, pour une moyenne de 5076,0 g. (Un 124e Gentoo a une masse vide, que l’étape field_as_float ignore — streamer vous permet d’écarter les mauvaises lignes au passage.) L’important, c’est quand le travail s’est produit : construire rows, gentoos et masses n’a rien lu — les générateurs sont paresseux, donc le fichier n’a pas été touché tant que la boucle for n’a pas commencé à tirer. Ensuite, chaque itération a tiré exactement une ligne à travers toute la chaîne (lire → filtrer → parser → additionner), puis l’a laissée filer. À aucun moment plus d’une seule ligne n’était en mémoire. Remplacez penguins.csv par un fichier de 50 Go et ce code est inchangé et tient toujours dans quelques kilo-octets — c’est le motif de streaming/ETL pour lequel les générateurs sont faits.

Pour compter tout le fichier de la même manière paresseuse, envoyez le lecteur directement dans sum :

print("total rows:", sum(1 for _ in read_rows("penguins.csv")))
total rows: 344

344 lignes, comptées en streamant un 1 par ligne dans sum — le fichier est lu une fois, une ligne à la fois, et jamais assemblé en une liste.

Performance et mémoire

Le compromis mérite d’être énoncé clairement, car il décide quand un générateur est le bon outil. Pour le voir, sommons un million de carrés de deux façons et mesurons l’empreinte mémoire de chacune. Pour la liste, nous comptons le conteneur et le million d’objets entiers qu’il pointe (sys.getsizeof sur la liste seule ne voit que le tableau de pointeurs) ; le générateur est mesuré tel quel :

import sys

# Eager: a real list holds the container PLUS a million integer objects
squares_list = [x * x for x in range(1_000_000)]
list_total = sys.getsizeof(squares_list) + sum(sys.getsizeof(v) for v in squares_list)

# Lazy: the generator is a fixed-size recipe, whatever the range
squares_gen = (x * x for x in range(1_000_000))
gen_total = sys.getsizeof(squares_gen)

total_eager = sum(squares_list)
total_lazy = sum(x * x for x in range(1_000_000))

print("same answer:", total_eager == total_lazy, "->", total_lazy)
print(f"list  footprint: {list_total:>12,} bytes  ({list_total / 1e6:.1f} MB)")
print(f"generator:       {gen_total:>12,} bytes")
print(f"the list holds ~{list_total // gen_total:,}x more")
same answer: True -> 333332833333500000
list  footprint:   40,317,656 bytes  (40.3 MB)
generator:                200 bytes
the list holds ~201,588x more

Résultat identique — 333332833333500000 dans les deux cas — mais l’empreinte réelle de la liste est de 40,3 Mo : le tableau de pointeurs d’~8 Mo vu plus haut plus le million d’objets entiers que ces pointeurs référencent. Le générateur fait 200 octets à plat, peu importe le nombre de valeurs qu’il produira à terme — plus de 200 000× plus petit — parce qu’il n’alloue jamais les valeurs du tout ; il ne détient que l’état en pause qui sait comment calculer la suivante. À cette échelle, la différence de mémoire est tout ce qui compte ; le calcul est essentiellement le même dans les deux cas.

Alors, quand chacune est-elle la bonne ?

  • Optez pour le paresseux (un générateur) quand les données sont volumineuses ou non bornées, que vous n’avez besoin de les parcourir qu’une fois, ou que vous voulez commencer à produire des résultats immédiatement — un générateur produit sa première valeur avant d’avoir calculé la deuxième, si bien qu’un pipeline peut commencer à émettre sa sortie avant que la source soit entièrement lue (idéal pour le streaming et la réactivité).
  • Optez pour le gourmand (une liste) quand vous avez besoin d’un accès aléatoire (data[100]), devez parcourir les valeurs plus d’une fois, avez besoin de len(), ou que le jeu de données est petit et que la commodité l’emporte. Matérialiser est un atout quand vous réutiliserez les résultats.

Un générateur n’est pas automatiquement plus rapide — le surcoût par élément est similaire, et si vous allez de toute façon construire une liste à la fin, une compréhension de liste est généralement un poil plus rapide. Le gain, c’est la mémoire et la latence, pas le débit brut. Choisissez le paresseux pour faire tenir des données volumineuses ou sans fin dans un espace constant et pour commencer le travail plus tôt ; choisissez le gourmand quand vous avez besoin d’avoir toute la collection sous la main pour la réutiliser.

Les générateurs asynchrones

Quand la source est asynchrone — des lignes arrivant d’une socket réseau, des pages d’une API paginée, des événements d’une file — la même idée a une forme async. Un générateur asynchrone est une fonction async def qui utilise yield, et vous le consommez avec async for au lieu d’un for ordinaire. Il vous permet d’await entre les éléments (pour que le programme puisse faire autre chose en attendant la valeur suivante) tout en streamant un élément à la fois.

import asyncio

async def read_stream(n):
    """Pretend to pull from an async source (a socket, an API page)."""
    for i in range(n):
        await asyncio.sleep(0)     # 'await' the next chunk without blocking
        yield i * 10               # ... then yield it, one at a time

async def main():
    async for value in read_stream(3):   # note: async for
        print(value)

asyncio.run(main())                # prints 0, 10, 20

La forme est le générateur ordinaire que vous connaissez déjà — une boucle avec un yield — avec deux marqueurs async/await ajoutés par-dessus : async def + yield en fait un générateur asynchrone, et async for le pilote. Vous en dégaineriez un pour streamer depuis une source d’E/S asynchrone sans mettre en tampon toute la réponse, exactement comme le pipeline synchrone plus haut mais pour des données awaitables. Les mécaniques de la concurrence (asyncio, la boucle d’événements, await) sont un sujet à part entière — une leçon ultérieure de cette série couvre la programmation asynchrone ; ici, il suffit de reconnaître le motif et de savoir que les générateurs s’étendent proprement au monde asynchrone.

Essayez en direct

Les blocs ci-dessus se sont exécutés au moment du build. Modifiez celui-ci et appuyez sur Run pour construire vous-même un pipeline paresseux — il s’exécute dans votre navigateur via Pyodide (aucune installation). Les générateurs sont du pur Python, donc cela tourne instantanément, sans rien à télécharger.

🟢 Avec un agent IA

Un fichier trop gros pour être chargé, ou une boucle qui construit une liste géante juste pour la sommer ? Collez votre code et demandez à Prova « réécris ceci pour streamer avec un générateur au lieu de tout charger » — ou « transforme ce lire/filtrer/résumer en un pipeline de générateurs enchaînés ». Elle écrit les fonctions yield, câble les étapes ensemble, et remplace la compréhension de liste gourmande en mémoire par une expression génératrice paresseuse. Parce que le runtime est juste là, vous l’exécutez et regardez la mémoire rester plate, au lieu de faire confiance au fait que la réécriture donne toujours la même réponse. The runtime is the judge. Demandez à Prova →

La même idée en R

Vous travaillez dans les deux langages ? R s’appuie sur des outils fonctionnels plutôt que sur un mot-clé yield. Là où Python enchaîne des générateurs, R enchaîne Filter(), Map() et Reduce() (base R) — ou le package purrr du tidyverse (map(), keep(), reduce()) — pour exprimer le même flux lire → filtrer → transformer → résumer. R n’a pas de générateurs paresseux de première classe de la même manière (sa paresse réside dans l’évaluation par promesse des arguments de fonction), donc pour des données vraiment volumineuses les utilisateurs de R streament typiquement avec un lecteur par blocs (readr::read_csv_chunked()) ou une connexion à une base de données via dbplyr. Le concept se transpose : gardez chaque étape petite, composez-les, et évitez de matérialiser tout le jeu de données quand vous n’avez besoin de le parcourir qu’une fois. Voir le pilier Programmation pour le versant R.

Problèmes fréquents

Un générateur est vide la deuxième fois que vous l’utilisez. Les générateurs sont à usage unique — une fois parcourus jusqu’au bout ils sont épuisés, et les reparcourir ne yield rien (vous avez vu list(g) renvoyer [] au second passage). Si vous avez besoin des valeurs plus d’une fois, soit recréez le générateur en rappelant la fonction (countdown(3)), soit matérialisez-les une fois dans une liste (values = list(gen)) et réutilisez la liste. Attention à la version sournoise : appeler sum(gen) puis max(gen) donne un max erroné sur un flux vide, parce que sum l’a déjà vidé.

Rien ne se passe — votre générateur « ne s’exécute pas ». Un générateur (ou une expression génératrice) est paresseux : le définir ou appeler la fonction ne fait aucun travail ; le corps ne s’exécute que lorsque quelque chose tire les valeurs avec next(), une boucle for, list(), sum(), etc. Si vos prints ou effets de bord ne se déclenchent jamais, c’est probablement que vous avez construit le générateur sans jamais l’itérer. Enveloppez-le dans list(...) (ou bouclez dessus) pour le forcer.

TypeError: object of type 'generator' has no len() — ou il refuse l’indexation. Un générateur n’a ni longueur ni positions, parce que ses valeurs n’existent pas encore — donc len(gen) et gen[0] lèvent tous deux TypeError (‘generator’ object is not subscriptable). Si vous avez réellement besoin d’une longueur ou d’un accès aléatoire, matérialisez d’abord (data = list(gen)), ou prenez une tranche paresseusement avec itertools.islice(gen, start, stop) au lieu de gen[start:stop].

Un return à l’intérieur d’un générateur l’arrête — il ne renvoie pas de valeur. Dans une fonction génératrice, return ne rend pas un résultat ; il met fin simplement à la génération (en levant StopIteration). Donc return x au milieu d’un générateur termine le flux prématurément et le x est écarté de l’itération normale (il est glissé dans la valeur de StopIteration, que vous lisez rarement). Utilisez yield pour émettre des valeurs et un return nu (ou tomber en fin de fonction) seulement pour arrêter.

Questions fréquentes

return met fin à une fonction et renvoie une seule valeur ; rappelez la fonction et elle repart du début. yield transforme la fonction en générateur : il renvoie une valeur et met en pause, figeant tout l’état local, et la requête suivante reprend sur la ligne après le yield. Ainsi return vous donne un seul résultat, tandis que yield permet à une fonction de produire toute une séquence de résultats paresseusement, un à la fois, en se souvenant d’où elle s’était arrêtée entre chacun. À l’intérieur d’un générateur, un return nu ne renvoie pas de valeur — il arrête simplement la génération.

Utilisez un générateur quand les données sont volumineuses ou non bornées, quand vous ne les parcourrez qu’une fois, ou quand vous voulez que les résultats commencent à s’écouler immédiatement (streamer un fichier, un flux réseau, un pipeline). Il maintient la mémoire plate — une seule valeur est vivante à la fois. Utilisez une liste quand vous avez besoin des valeurs plus d’une fois, d’un accès aléatoire (data[i]) ou de len(), ou que le jeu de données est assez petit pour que la commodité l’emporte. Règle empirique : parcouru-une-fois et potentiellement-énorme → générateur ; réutilisé ou indexé → liste.

Un itérateur est tout objet implémentant le protocole d’itération — __iter__ (se renvoie lui-même) et __next__ (renvoie la valeur suivante ou lève StopIteration). Un générateur est la façon facile d’en fabriquer un : toute fonction contenant yield renvoie un générateur, qui est un itérateur dont le __next__ est écrit pour vous par la pause/reprise de yield. Ainsi tout générateur est un itérateur, mais tout itérateur n’est pas un générateur (vous pouvez aussi en écrire un à la main sous forme de classe). En pratique, vous créez presque toujours des itérateurs avec yield ou une expression génératrice plutôt qu’avec le code répétitif d’une classe.

Une expression génératrice est une compréhension avec des parenthèses rondes au lieu de crochets : (x*x for x in data) au lieu de [x*x for x in data]. Elle produit un générateur paresseux — calculant chaque valeur seulement quand on la demande — plutôt que de construire toute la liste d’avance, si bien qu’elle utilise une mémoire quasi constante. Quand c’est l’unique argument d’une fonction, vous pouvez omettre les parenthèses supplémentaires : sum(x*x for x in data). Préférez-la à une compréhension de liste dès que vous consommerez les valeurs une fois (en alimentant sum/min/max/any/all/join ou une boucle for) et que vous n’avez pas besoin de les indexer ni de les réutiliser.

Un générateur asynchrone est une fonction async def qui utilise yield ; vous le consommez avec async for au lieu d’un for ordinaire. Il vous permet d’await entre les éléments — pour que le programme puisse faire autre chose en attendant la valeur suivante — tout en streamant un élément à la fois. Dégainez-en un pour lire depuis une source asynchrone (une socket réseau, une API paginée, une file d’événements) sans mettre en tampon toute la réponse en mémoire. C’est l’homologue asyncio d’un générateur ordinaire ; la machinerie de la concurrence (la boucle d’événements, await) est un sujet plus large couvert dans la leçon sur la programmation asynchrone de cette série.

Testez vos connaissances

Écrivez un pipeline paresseux qui streame des nombres, ne garde que ceux divisibles par 3, les met au carré, et somme le résultat — sans jamais construire de liste des valeurs intermédiaires.

Tâche 1. Écrivez une fonction génératrice multiples_of(step, n) qui produit step, 2*step, … jusqu’à n (exclu). Puis, à l’aide d’expressions génératrices ou de petites fonctions génératrices, calculez la somme des carrés de chaque multiple de 3 en dessous de 1000. Vérifiez que vous ne matérialisez jamais les nombres dans une liste.

Tâche 2. Expliquez en une phrase pourquoi le pipeline n’utilise presque pas de mémoire même si vous montez la limite de 1000 à 1 000 000 000.

Pour la Tâche 1, multiples_of est une boucle while/yield tout comme countdown. Pour la somme, envoyez une expression génératrice directement dans sum : sum(x * x for x in multiples_of(3, 1000)) — les parenthèses peuvent être omises parce que l’expression génératrice est l’unique argument de sum. Rien n’est stocké parce que sum tire une valeur à la fois.

def multiples_of(step, n):
    value = step
    while value < n:
        yield value
        value += step

# Stream -> square -> sum, one value alive at a time (no list built)
answer = sum(x * x for x in multiples_of(3, 1000))
print(answer)   # 111277611

multiples_of(3, 1000) produit 3, 6, 9, … 999 un à la fois ; l’expression génératrice met chacun au carré à mesure qu’il arrive ; et sum les additionne sans jamais détenir la séquence. Tâche 2 : la mémoire reste plate parce que seule la valeur courante existe à un instant donné — le générateur détient une recette (son état en pause), pas les valeurs, si bien qu’une limite d’un milliard coûte les mêmes quelques centaines d’octets qu’une limite de mille.

Vérification rapide. Vous exécutez g = (x for x in range(5)), puis print(sum(g)), puis print(max(g)). La somme affiche 10, mais max(g) lève ValueError: max() iterable argument is empty. Pourquoi ?

Un générateur est à usage unique. sum(g) a parcouru g jusqu’au bout et l’a épuisé, si bien qu’au moment où max(g) s’exécute il ne reste aucune valeur — d’où « empty sequence ». Corrigez-le en matérialisant une fois (data = list(g), puis sum(data) et max(data)) ou en créant un nouveau générateur pour chaque passage.

Conclusion

Vous savez maintenant ce qu’un générateur est réellement et quand y recourir. Une boucle for tourne sur le protocole d’itération (iter/next) ; yield vous donne un itérateur gratuitement en mettant en pause et en reprenant une fonction, conservant son état sans stocker chaque valeur ; une expression génératrice est la cousine paresseuse, à mémoire quasi nulle, d’une compréhension de liste ; itertools fournit les briques paresseuses (islice, chain, groupby, accumulate, et les infinis count/cycle/repeat) ; et enchaîner de petits générateurs en un pipeline vous permet de streamer un fichier bien plus gros que la mémoire, une ligne à la fois. Rappelez-vous les deux règles qui piègent tout le monde : un générateur est à usage unique (épuisé après un passage), et il est paresseux (rien ne s’exécute tant que vous n’itérez pas). Recourez à un générateur quand les données sont volumineuses, sans fin, ou parcourues-une-fois ; recourez à une liste quand vous devez la réutiliser ou l’indexer.

Leçons connexes

  • Poursuivez avec : Compréhensions Python — une expression génératrice n’est qu’à un crochet de la compréhension de liste que vous connaissez déjà · Fichiers, CSV et JSON — lire des fichiers est précisément là où le streaming paresseux paie · Types et structures de données — les itérables à partir desquels les générateurs produisent et qu’ils alimentent.
  • Allez plus loin : les leçons ultérieures de cette série Python Intermédiaire s’appuient sur les générateurs — la programmation asynchrone (les générateurs asynchrones et la boucle d’événements), et le traitement parallèle (un générateur alimente proprement un pool de workers). Voir le pilier Programmation pour l’ensemble complet des leçons R et Python.
Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Python intermédiaire — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Générateurs Python : yield, genexpr et itertools},
  date = {2026-07-17},
  url = {https://www.datanovia.com/learn/programming/python-intermediate/iterators-generators},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Générateurs Python : yield, genexpr et itertools.” 2026. July 17. https://www.datanovia.com/learn/programming/python-intermediate/iterators-generators.