W3docs

Générateurs Python et yield

Apprenez les générateurs Python et le mot-clé yield avec des exemples clairs couvrant les fonctions génératrices, les expressions, send() et des cas d'usage réels.

Un générateur est un type spécial d'itérateur qui produit des valeurs une à une, à la demande, au lieu de toutes les calculer d'emblée. Les générateurs sont définis avec la syntaxe habituelle des fonctions en remplaçant return par yield. Ils constituent la solution Python idiomatique pour les séquences volumineuses ou infinies, où construire une liste complète gaspillerait de la mémoire ou du temps.

Ce chapitre couvre le mot-clé yield, les fonctions génératrices par rapport aux listes, les expressions génératrices, l'envoi de valeurs dans un générateur, le chaînage de générateurs et des motifs courants en pratique.

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

Quand Python appelle une fonction ordinaire, il exécute le corps jusqu'à la fin et retourne une valeur. Quand Python appelle une fonction génératrice, il n'exécute pas le corps du tout — il retourne un objet générateur. Chaque fois que vous appelez next() sur cet objet, l'exécution reprend là où elle s'est arrêtée (à l'instruction yield), avance jusqu'au prochain yield, puis se suspend à nouveau.

def count_up(start, stop):
    while start <= stop:
        yield start        # pause here, emit the value
        start += 1

gen = count_up(1, 3)
print(next(gen))   # 1
print(next(gen))   # 2
print(next(gen))   # 3
# next(gen) would now raise StopIteration

Mécanismes clés :

  • Le corps de la fonction ne s'exécute pas avant le premier appel à next().
  • Les variables locales et le pointeur d'instruction sont préservés entre les appels.
  • Quand le corps de la fonction se termine (ou rencontre un return nu), Python lève StopIteration automatiquement.
  • Une boucle for appelle next() pour vous et s'arrête proprement sur StopIteration.

Le mot-clé yield

yield est la seule syntaxe qui distingue une fonction génératrice d'une fonction ordinaire. Vous pouvez utiliser yield partout où un return pourrait apparaître, notamment dans des boucles, des conditionnelles et des blocs try/except.

yield vs return

returnyield
Type de fonctionOrdinaireGénératrice
Exécution après l'appelS'exécute jusqu'à la finS'interrompt à yield
État entre les appelsPerduPréservé
Valeurs multiplesUne seule (ou un tuple)Une par yield, séquentiellement
Mémoire pour de grandes donnéesContient toutes les valeursContient une valeur à la fois

yield Suspend, Ne Termine Pas

def three_things():
    print("about to yield first")
    yield "first"
    print("about to yield second")
    yield "second"
    print("about to yield third")
    yield "third"
    print("generator exhausted")

for item in three_things():
    print("got:", item)

Sortie :

about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhausted

Notez les instructions print entre les yields — le code normal s'exécute entre chaque suspension.

Fonctions génératrices vs Listes

Considérons la génération des n premiers nombres au carré. Avec une liste :

def squares_list(n):
    result = []
    for i in range(1, n + 1):
        result.append(i * i)
    return result

print(squares_list(5))   # [1, 4, 9, 16, 25]

Avec un générateur :

def squares_gen(n):
    for i in range(1, n + 1):
        yield i * i

gen = squares_gen(5)
print(list(gen))         # [1, 4, 9, 16, 25]

Les deux produisent les mêmes valeurs, mais la version avec générateur :

  • Utilise une mémoire O(1) quelle que soit la valeur de n (la version liste utilise O(n))
  • Commence à produire des valeurs immédiatement, sans attendre de construire toute la collection
  • Peut représenter des séquences infinies (ce qu'une liste ne peut pas faire)

Quand choisir un générateur

Utilisez un générateur quand :

  • Vous n'avez besoin d'itérer qu'une seule fois sur les valeurs.
  • La séquence est suffisamment grande pour que la conserver entièrement en mémoire soit problématique.
  • Vous construisez un pipeline de données (un générateur alimente un autre).
  • La séquence est potentiellement infinie (par exemple, la lecture de lignes de log depuis un fichier actif).

Utilisez une liste quand :

  • Vous avez besoin d'un accès aléatoire par index.
  • Vous devez itérer plusieurs fois sur la même séquence.
  • Vous avez besoin de len(), du découpage ou du tri en place.

Expressions génératrices

Une expression génératrice est aux générateurs ce qu'une compréhension de liste est aux listes. La syntaxe est identique, sauf qu'on utilise des parenthèses à la place des crochets :

# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]

# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))

print(type(squares_list))   # <class 'list'>
print(type(squares_gen))    # <class 'generator'>

print(list(squares_gen))    # [1, 4, 9, 16, 25]

Les expressions génératrices sont particulièrement utiles lorsqu'elles sont passées directement à une fonction qui consomme un itérable :

total = sum(x * x for x in range(1, 101))   # sum of squares 1..100
print(total)   # 338350

Pas besoin de parenthèses supplémentaires quand l'expression génératrice est le seul argument d'un appel de fonction.

Filtrage avec les expressions génératrices

evens = (x for x in range(20) if x % 2 == 0)
print(list(evens))   # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Générateurs infinis

Parce qu'un générateur produit des valeurs de façon paresseuse, il peut représenter une séquence sans fin. L'exemple classique est un compteur infini :

def counter(start=0):
    n = start
    while True:
        yield n
        n += 1

gen = counter(10)
print(next(gen))   # 10
print(next(gen))   # 11
print(next(gen))   # 12

Pour ne consommer qu'une partie d'un générateur infini, utilisez itertools.islice ou sortez d'une boucle avec break :

import itertools

gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five)   # [1, 2, 3, 4, 5]

Un générateur infini pratique — la suite de Fibonacci :

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

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

yield from — Déléguer à un sous-générateur

yield from permet à un générateur de déléguer à un autre itérable, en transmettant chaque valeur de façon transparente :

def first_part():
    yield 1
    yield 2

def second_part():
    yield 3
    yield 4

def combined():
    yield from first_part()
    yield from second_part()

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

yield from fonctionne également avec n'importe quel itérable, pas seulement des générateurs :

def flatten(nested):
    for sublist in nested:
        yield from sublist

data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data)))   # [1, 2, 3, 4, 5, 6]

yield from est plus lisible qu'une boucle for imbriquée sur le sous-itérable, et il transmet correctement les appels send() et throw() au générateur délégué (important pour les motifs de coroutines).

Envoyer des valeurs dans un générateur

Les générateurs sont des canaux bidirectionnels. La méthode .send(value) reprend le générateur et lui passe une valeur en retour comme résultat de l'expression yield :

def accumulator():
    total = 0
    while True:
        value = yield total   # yield sends total out; receives value in
        if value is None:
            break
        total += value

gen = accumulator()
next(gen)          # prime the generator (advance to first yield)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

Règles pour .send() :

  1. Vous devez appeler next(gen) (ou gen.send(None)) une fois pour faire avancer le générateur jusqu'au premier yield avant de pouvoir envoyer une valeur autre que None.
  2. send(None) est équivalent à next().
  3. La valeur envoyée devient le résultat de l'expression yield du côté gauche.

État d'un générateur et épuisement

Un objet générateur a un cycle de vie avec quatre états :

ÉtatDescription
CrééLa fonction génératrice a été appelée, le corps n'a pas encore démarré
En coursEn cours d'exécution (à l'intérieur d'un appel next() ou send())
SuspenduEn pause à un yield ; reprendra au prochain next()
FerméLe corps est terminé ou .close() a été appelé ; lève StopIteration

Une fois épuisé, itérer à nouveau un générateur ne produit rien :

gen = (x for x in range(3))
print(list(gen))   # [0, 1, 2]
print(list(gen))   # []  — already exhausted

Si vous devez itérer plus d'une fois sur la sortie d'un générateur, convertissez-le d'abord en liste ou recréez le générateur.

return à l'intérieur d'un générateur

Une instruction return à l'intérieur d'un générateur termine l'itération proprement. La valeur passée à return devient l'attribut value de l'exception StopIteration (rarement utilisé directement, mais important pour la délégation avec yield from) :

def limited():
    yield 1
    yield 2
    return "done"    # StopIteration.value = "done"

gen = limited()
print(next(gen))   # 1
print(next(gen))   # 2
try:
    next(gen)
except StopIteration as e:
    print(e.value)  # done

Motifs courants en pratique

Lecture d'un grand fichier ligne par ligne

def read_lines(filepath):
    with open(filepath) as f:
        for line in f:
            yield line.rstrip("\n")

# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
    if line.startswith("#"):
        continue
    print(line)

Construction d'un pipeline de données

Les générateurs se composent naturellement en pipelines où chaque étape transforme le flux :

def integers(n):
    for i in range(1, n + 1):
        yield i

def only_even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n * n

# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

Chaque étape est paresseuse — les valeurs traversent le pipeline une à une sans construire de listes intermédiaires.

Découpage d'un itérable en blocs

def chunks(iterable, size):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

data = list(range(10))
for batch in chunks(data, 3):
    print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]

Générateurs vs Itérateurs vs Compréhensions

CaractéristiqueClasse itérateurFonction génératriceExpression génératrice
SyntaxeClasse avec __iter__/__next__def + yield(expr for x in ...)
VerbositéÉlevéeFaibleTrès faible
Gestion de l'étatManuelleAutomatiqueAutomatique
Logique multi-instructionsOuiOuiNon (expression unique)
Séquences infiniesOuiOuiOui
Lisibilité pour une logique complexeOuiOuiNon

Pour tout ce qui dépasse une simple transformation ou un filtrage, une fonction génératrice est plus lisible qu'une expression génératrice. Pour une itération avec état complexe, une fonction génératrice est presque toujours préférable à l'écriture d'une classe itérateur complète — voir Itérateurs Python pour l'approche basée sur les classes.

Les expressions génératrices s'associent naturellement aux compréhensions de liste et aux compréhensions de dictionnaire/ensemble. Les décorateurs peuvent également envelopper des fonctions génératrices pour ajouter un comportement de mise en cache ou de traçage.

Pratique

Pratique
Which of the following statements about Python generators are correct?
Which of the following statements about Python generators are correct?
Was this page helpful?