yield

Qu'est-ce que le mot-clé yield en Python ?

Le mot-clé yield est utilisé à l'intérieur d'une fonction. On parle alors de fonction génératrice, qui renvoie elle-même un générateur. Contrairement à une fonction classique qui s'arrête et renvoie une valeur finale avec return, une fonction génératrice met son exécution en pause, renvoie une valeur intermédiaire et se souvient de son état pour reprendre là où elle en était à la prochaine itération.

Comment fonctionne un générateur ?

Un générateur est en réalité un objet itérable. Pour interagir manuellement avec lui, Python utilise les fonctions natives iter() et next() :

  • La fonction iter() permet d'obtenir un itérateur à partir d'un objet

  • La fonction next() demande à l'itérateur de calculer et de renvoyer la valeur suivante

À noter

Itérable vs Itérateur vs Générateur 🤯.

On va reprendre terme par terme :

  • Itérable : Un objet que l'on peut parcourir (comme une liste). Il possède la méthode __iter__() (ce qui permet d'appeler iter() dessus), mais ne possède pas la méthode __next__()

  • Itérateur : L'objet renvoyé par iter() qui effectue réellement le parcours. Il possède obligatoirement les deux méthodes __iter__() et __next__()

  • Générateur : Un itérateur spécifique. Parce qu'il est créé via yield (ou une expression génératrice), il est déjà un itérateur dès sa création. Il possède nativement __next__() et __iter__(). Il est donc à la fois un itérable et un itérateur

Un générateur est un itérateur, et donc aussi un itérable. Sa particularité est qu'il génère les valeurs à la demande plutôt que de parcourir des valeurs déjà existantes en mémoire.

def compteur_personnalise(maximum):
    print("Début du compteur...")
    compteur = 1
    while compteur <= maximum:
        yield compteur  # Pause ici
        compteur += 1

# Création du générateur
mon_compteur = compteur_personnalise(2)  # Rien ne s'exécute ici ! (Lazy evaluation)

# L'exécution du corps de la fonction démarre seulement au premier appel de next()
print(next(mon_compteur))  # Affiche : Début du compteur... puis 1
print(next(mon_compteur))  # Affiche : 2

# Si on appelle next() une 3ème fois :
# print(next(mon_compteur)) -> Lève une exception StopIteration
PYTHON

On est d'accord que gérer une exception StopIteration manuellement est fastidieux. Heureusement, vous connaissez la fameuse boucle for qui gère tout cela automatiquement.

def compteur_personnalise(maximum):
    print("Début du compteur...")
    compteur = 1
    while compteur <= maximum:
        yield compteur  # Pause ici
        compteur += 1

for nombre in compteur_personnalise(5):
    print(f"Compteur actuel : {nombre}")
    # Compteur actuel : 1
    # ...
    # Compteur actuel : 5
PYTHON

La boucle s'arrête proprement.

Les expressions génératrices

Vous connaissez certainement les compréhensions de liste. Vous serez heureux de savoir qu'il existe un équivalent pour les générateurs : les expressions génératrices.

Il suffit de remplacer les [] par des () :

# Compréhension de liste (crée la liste entière en mémoire)
liste_carres = [x**2 for x in range(10)]

# Expression génératrice (utilise yield en arrière-plan)
generateur_carres = (x**2 for x in range(10))
PYTHON

Astuce simple et concise 😁.

yield from

yield from permet à un générateur de déléguer une partie des opérations à un autre itérable. Cela permet d'éviter les boucles for imbriquées :

def sous_generateur():
    yield "A"
    yield "B"

def generateur_principal():
    yield 1
    # Au lieu de faire :
    # for element in sous_generateur():
    #     yield element
    yield from sous_generateur()  # Délégation directe !
    yield 2

for valeur in generateur_principal():
    print(valeur)

# Affiche : 1, A, B, 2
PYTHON

C'est particulièrement pratique pour parcourir des structures récursives :

def flatten(data):
    for item in data:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

list(flatten([1, [2, [3, 4]], 5]))  # → [1, 2, 3, 4, 5]
PYTHON

Pourquoi utiliser yield ?

yield est particulièrement utile pour le traitement de données volumineuses. Au lieu de tout charger en mémoire, le générateur produit les valeurs une par une, à la demande :

import sys

# La liste stocke les 100 000 éléments en mémoire
liste = [x**2 for x in range(100_000)]

# Le générateur ne stocke que la logique de calcul
generateur = (x**2 for x in range(100_000))

print(sys.getsizeof(liste))      # ~800 000 octets (800 Ko)
print(sys.getsizeof(generateur)) # ~200 octets, et ce, peu importe la taille !
PYTHON

À noter

sys.getsizeof renvoie la taille de la liste et de ses pointeurs, mais ne suit pas ces pointeurs jusqu'aux objets référencés. La RAM réellement consommée est donc supérieure.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.