Apprenez en lisant dans l'ordre

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)
Comment yield et next() s'articulent
gen = simple()(pas encore en cours)objet generator créénext(gen) → s'arrête sur yield 1→ retourne 1next(gen) → s'arrête sur yield 2→ retourne 2next() → plus de yield, StopIteration1ère fois2e foisà partir de la 3e

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.

Construis une fonction génératrice qui yield des IDs de commande 1, 2, 3 dans l'ordre, et récupère-les un par un avec next().

① Définis def order_ids(): avec trois lignes : yield 1 / yield 2 / yield 3.

② Construis un objet generator : gen = order_ids().

③ Appelle print(next(gen)) trois fois d'affilée et vérifie que 1 / 2 / 3 apparaissent.

(Lorsque la réponse est correcte, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

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.

Mariage avec une boucle for
for v in gen:Côté générateuryield retourneet met en pauseCôté appelantLe corps de boucleutilise la valeurArrêt autoquand lesyields s'épuisentnextvalue

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)

Utilise for pour recevoir des données client depuis un générateur qui les streame une à une.

① Définis def each_customer(): et boucle avec for name in ["Alice", "Léo", "Léa"]:, en yieldant name.

② Récupère les valeurs avec for name in each_customer(): et affiche f"Client suivant : {name}".

Si les trois apparaissent dans l'ordre, c'est gagné.

Éditeur Python

Exécuter le code pour voir le résultat

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.

Empreinte mémoire list vs generator
list[0, 1, 2, ..., 999999]Plusieurs Mochargés d'un coupBien quand tu as besoinde tout d'un coupgenerator(i for i in range(...))Quelques centaines d'octets(que l'élément courant)Bien pour streamerun à un
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.

Construis une expression génératrice (une compréhension de liste avec des parenthèses au lieu de crochets) pour des données de prix, vérifie le type avec type(), puis récupère les valeurs une à une avec for.

① Construis prices = (base * 100 for base in range(1, 6)). L'astuce, c'est d'utiliser ( ) au lieu de [ ].

② Affiche le type avec print(type(prices)) et vérifie que <class 'generator'> apparaît.

③ Récupère les valeurs avec for p in prices: et affiche f"Prix : {p}".

Éditeur Python

Exécuter le code pour voir le résultat

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 délègue à un sous-générateur
Côté appelantfor amountin all_sales()all_sales()générateurprincipalOrdre reçu1200 → 980→ 850 → 1340①yield fromtokyo_sales()tokyo_sales()yield 1200yield 980②yield fromosaka_sales()osaka_sales()yield 850yield 1340pilotedélèguesuivant quand finidélègue

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():.

Construis un générateur qui combine des listes d'inventaire par magasin en un seul flux.

① Définis def store_a(): et for item in ["pomme", "orange"]: yield item.

② Définis def store_b(): et for item in ["banane", "raisin", "fraise"]: yield item.

③ Définis def all_items(): et écris yield from store_a() suivi de yield from store_b().

④ Itère avec for item in all_items(): print(item) pour afficher les 5 dans l'ordre.

Éditeur Python

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Quelle est la correspondance la plus proche pour print(type(gen)) ici ?
def f():
yield 1
yield 2
gen = f()
print(type(gen))

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))