Question 1Quelle est la correspondance la plus proche pour print(type(gen)) ici ?def f():
yield 1
yield 2
gen = f()
print(type(gen))
Fonctions génératrices avec yield — produire des valeurs une à une pour économiser la mémoire
Apprends les fonctions génératrices Python avec yield pour produire des valeurs une à une et économiser la mémoire.
Dans l'article précédent, tu as vu les clôtures — comment enfermer un état dans une fonction et retourner une valeur différente à chaque appel. Python propose un mécanisme dédié à ce genre de fonction « retourne la prochaine valeur à chaque appel » : la fonction génératrice. Utilise yield au lieu de return, et la fonction se met en pause au milieu pour retourner une valeur, puis reprend là où elle s'est arrêtée au prochain appel.
C'est idéal quand tu ne veux pas matérialiser une grande collection d'un coup et que tu préfères streamer une valeur à la fois — traitement de logs, calculs sur de gros volumes, et charges similaires.
Les bases de yield — retourner une valeur à la fois
Une fonction qui contient yield value dans son corps est une fonction génératrice. L'appeler comme une fonction normale n'exécute pas le corps — tu récupères simplement un objet generator spécial.
Pour vraiment en extraire une valeur, appelle next(obj).
Le premier next() exécute le corps jusqu'au premier yield et retourne sa valeur.
Le next() suivant reprend de là et va jusqu'au prochain yield.
Une fois qu'il n'y a plus de yield, un nouvel appel à next() lève StopIteration.
def simple():
yield 1
yield 2
gen = simple()
print(type(gen)) # <class 'generator'>
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) ← StopIteration (plus de yield)
Différence avec return
Une fonction normale return et toute la portée disparaît — l'appel suivant repart du début. Une fonction génératrice, en revanche, se met en pause sur yield et garde les locales et la position. Le prochain next() reprend pile où tu t'étais arrêté, et c'est ça la grosse différence.
Récupérer les valeurs avec for — pas besoin de gérer StopIteration
Écrire next() à chaque fois est lassant, et tu n'as pas non plus à gérer toi-même StopIteration. Utilise plutôt for value in generator:, et Python appelle next() en coulisses et sort automatiquement de la boucle quand le générateur est fini. C'est de loin la forme la plus courante.
Si tu saupoudres des print("en cours...") des deux côtés, tu peux observer chaque yield faire basculer l'exécution dans un sens et dans l'autre — côté générateur → côté appelant → côté générateur.
def count_up_to(max_value):
print("démarrage du générateur")
for i in range(max_value):
print(f" avant yield : {i}")
yield i
print(f" après yield : {i}")
for v in count_up_to(3):
print(f"reçu : {v}")
# Flux de sortie :
# démarrage du générateur
# avant yield : 0
# reçu : 0
# après yield : 0
# avant yield : 1
# ... (continue)
Différence avec list — réduire la consommation mémoire
Quand tu travailles sur des valeurs de 0 à 999_999, une compréhension de liste matérialise les 1 000 000 d'entiers d'un coup en mémoire. Un générateur, en revanche, ne garde que la valeur courante et calcule la suivante à la demande. La liste finit dans l'ordre de plusieurs Mo ; l'objet générateur lui-même ne fait que quelques centaines d'octets.
import sys
MAX = 10 ** 6
# Liste : charge tout en mémoire d'un coup
data_list = [i for i in range(MAX)]
print(sys.getsizeof(data_list))
# par ex. 8000056 (~8 Mo)
# Expression génératrice : seulement l'élément courant
data_gen = (i for i in range(MAX))
print(sys.getsizeof(data_gen))
# par ex. ~200 octets
# Le code côté appelant est identique
for v in data_gen:
if v > 2:
break
print(v)
# 0
# 1
# 2
Le raccourci de l'expression génératrice
Échange les crochets [ ... ] d'une compréhension de liste contre des parenthèses ( ... ) et tu as une expression génératrice. (i for i in range(1_000_000)) te donne en une ligne le même effet qu'une fonction génératrice à base de def. Passe-la directement à sum() / max() / any() et compagnie — ça marche tel quel.
Chaîner des générateurs avec yield from
Quand tu veux qu'un générateur transmette les valeurs d'un autre générateur telles quelles, tu peux écrire yield from sub_generator en une ligne au lieu de for v in sub: yield v. C'est pratique quand tu veux fusionner plusieurs sources de données en un seul générateur.
Par exemple, avec une fonction qui streame les ventes de la branche de Tokyo et une autre pour Osaka, aligner yield from tokyo_sales() et yield from osaka_sales() donne à l'appelant un générateur qui ressemble à un seul flux continu.
def tokyo_sales():
yield 1200
yield 980
def osaka_sales():
yield 850
yield 1340
def all_sales():
yield from tokyo_sales()
yield from osaka_sales()
for amount in all_sales():
print(amount)
# 1200
# 980
# 850
# 1340
yield from sub_gen() est un raccourci pour for v in sub_gen(): yield v.
Les valeurs yieldées par le sous-générateur vont directement à l'appelant externe, donc
les 1200, 980 de tokyo_sales,
puis les 850, 1340 de osaka_sales
arrivent dans l'ordre à for amount in all_sales():.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Que se passe-t-il quand tu appelles next() sur un générateur après que tous les yield ont été utilisés ?
Question 3Laquelle de ces deux lignes utilise dramatiquement moins de mémoire ?
A : data = [i for i in range(10**6)]
B : data = (i for i in range(10**6))