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 StopIterationMé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
returnnu), Python lèveStopIterationautomatiquement. - Une boucle
forappellenext()pour vous et s'arrête proprement surStopIteration.
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
return | yield | |
|---|---|---|
| Type de fonction | Ordinaire | Génératrice |
| Exécution après l'appel | S'exécute jusqu'à la fin | S'interrompt à yield |
| État entre les appels | Perdu | Préservé |
| Valeurs multiples | Une seule (ou un tuple) | Une par yield, séquentiellement |
| Mémoire pour de grandes données | Contient toutes les valeurs | Contient 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 exhaustedNotez 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) # 338350Pas 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)) # 12Pour 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)) # 35Règles pour .send() :
- Vous devez appeler
next(gen)(ougen.send(None)) une fois pour faire avancer le générateur jusqu'au premieryieldavant de pouvoir envoyer une valeur autre queNone. send(None)est équivalent ànext().- La valeur envoyée devient le résultat de l'expression
yielddu côté gauche.
État d'un générateur et épuisement
Un objet générateur a un cycle de vie avec quatre états :
| État | Description |
|---|---|
| Créé | La fonction génératrice a été appelée, le corps n'a pas encore démarré |
| En cours | En cours d'exécution (à l'intérieur d'un appel next() ou send()) |
| Suspendu | En 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 exhaustedSi 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) # doneMotifs 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éristique | Classe itérateur | Fonction génératrice | Expression génératrice |
|---|---|---|---|
| Syntaxe | Classe avec __iter__/__next__ | def + yield | (expr for x in ...) |
| Verbosité | Élevée | Faible | Très faible |
| Gestion de l'état | Manuelle | Automatique | Automatique |
| Logique multi-instructions | Oui | Oui | Non (expression unique) |
| Séquences infinies | Oui | Oui | Oui |
| Lisibilité pour une logique complexe | Oui | Oui | Non |
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.