
Utiliser le mot-clé yield pour créer des générateurs
Découvrez les générateurs en Python, une façon simple et efficace de créer des itérateurs. Apprenez à utiliser le mot-clé 'yield', comprenez les avantages des générateurs (évaluation paresseuse, efficacité mémoire).
Qu'est-ce qu'un générateur ? Un itérateur simplifié
En Python, un générateur est une fonction spéciale qui retourne un itérateur. Au lieu de retourner une seule valeur (avec `return`), un générateur *produit* une séquence de valeurs au fil du temps, en utilisant le mot-clé `yield`.
Les générateurs sont un moyen simple et puissant de créer des itérateurs. Ils sont particulièrement utiles pour :
- Générer des séquences potentiellement infinies.
- Traiter des données volumineuses qui ne tiennent pas en mémoire.
- Implémenter des pipelines de traitement de données (chaînage de générateurs).
- Simplifier le code qui produirait autrement des itérateurs complexes.
Les générateurs utilisent l'évaluation paresseuse ("lazy evaluation"), ce qui signifie qu'ils ne calculent les valeurs qu'au moment où elles sont demandées. Cela les rend très efficaces en termes de mémoire.
Le mot-clé yield : suspendre et reprendre l'exécution
Le mot-clé `yield` est ce qui distingue une fonction génératrice d'une fonction ordinaire. Lorsqu'une fonction contient `yield`, elle devient automatiquement une fonction génératrice.
Lorsqu'une fonction génératrice est appelée, elle ne s'exécute pas immédiatement. Au lieu de cela, elle retourne un objet générateur (un itérateur).
Lorsque la méthode `__next__` de l'itérateur (ou la fonction `next()`) est appelée :
- La fonction génératrice s'exécute jusqu'à ce qu'elle rencontre une instruction `yield`.
- L'instruction `yield` "produit" une valeur (la valeur qui suit le mot-clé `yield`). L'état de la fonction (variables locales, point d'exécution) est *suspendu*.
- La valeur produite est retournée à l'appelant.
- Lors de l'appel suivant à `__next__`, l'exécution de la fonction génératrice reprend *juste après* l'instruction `yield` qui avait été exécutée, et continue jusqu'à la prochaine instruction `yield` (ou jusqu'à la fin de la fonction).
Si la fonction génératrice se termine (sans rencontrer de `yield`), ou si elle exécute une instruction `return`, une exception `StopIteration` est levée, signalant la fin de l'itération.
Exemple : Un générateur simple qui produit les nombres de 0 à n-1 :
def nombres(n):
"""Générateur qui produit les nombres de 0 à n-1."""
for i in range(n):
yield i
# Utilisation du générateur
g = nombres(5) # g est un objet générateur
for i in g:
print(i) # 0 1 2 3 4Dans cet exemple, `nombres` est une fonction génératrice. Lorsqu'elle est appelée, elle retourne un objet générateur (`g`). La boucle `for` appelle implicitement `next(g)` à chaque itération, ce qui exécute la fonction `nombres` jusqu'à la prochaine instruction `yield`. La valeur produite par `yield` est affectée à `i` et utilisée dans le corps de la boucle `for`.
Avantages des générateurs : évaluation paresseuse et efficacité mémoire
Les générateurs présentent plusieurs avantages, principalement liés à l'évaluation paresseuse (lazy evaluation) :
- Efficacité mémoire : Les générateurs ne stockent pas tous les éléments de la séquence en mémoire en même temps. Ils génèrent les éléments un par un, à la demande. Cela est particulièrement important pour les séquences très grandes ou infinies.
- Evaluation paresseuse : Les éléments ne sont calculés qu'au moment où ils sont nécessaires. Cela peut améliorer les performances, surtout si vous n'avez pas besoin de tous les éléments de la séquence.
- Possibilité de représenter des séquences infinies : Vous pouvez créer des générateurs qui produisent des séquences infinies, ce qui serait impossible avec des listes.
- Composition : Vous pouvez chaîner des générateurs pour créer des pipelines de traitement de données complexes.
Exemple illustrant l'efficacité mémoire :
# Créer une liste de 1 million de nombres prendrait beaucoup de mémoire
# liste_nombres = [x for x in range(1000000)]
# Un générateur qui produit les mêmes nombres prend très peu de mémoire
def generateur_nombres(n):
for i in range(n):
yield i
g = generateur_nombres(1000000)
# On peut parcourir le générateur sans saturer la mémoire
# for i in g:
# passExemples de générateurs
Voici quelques exemples de générateurs :
Générateur pour les nombres pairs :
def nombres_pairs(n):
for i in range(0, n, 2):
yield iGénérateur pour lire un fichier ligne par ligne (Python le fait déjà implicitement, mais c'est un bon exemple) :
def lire_lignes(nom_fichier):
with open(nom_fichier, 'r') as f:
for ligne in f:
yield ligneGénérateur pour les nombres premiers :
def nombres_premiers():
yield 2
primes = [2]
n = 3
while True:
is_prime = True
for p in primes:
if p * p > n:
break
if n % p == 0:
is_prime = False
break
if is_prime:
primes.append(n)
yield n
n += 2Générateur pour "aplatir" une liste de listes :
def aplatir(liste_de_listes):
for sous_liste in liste_de_listes:
for element in sous_liste:
yield elementreturn dans un générateur
Une subtilité à connaître concerne l'utilisation de `return` dans un générateur.
Si une fonction génératrice contient une instruction `return`, cette instruction met fin à l'itération (en levant implicitement `StopIteration`).
En Python 3.3 et versions ultérieures, vous pouvez utiliser `return valeur` dans un générateur. La `valeur` ne sera *pas* produite par le générateur, mais elle sera disponible comme valeur de l'exception `StopIteration`.
Cependant, il est important de noter que cette valeur ne sera pas accessible directement si on utilise le générateur dans une boucle `for`. En général, il est recommandé d'utiliser `return` sans valeur dans un générateur, ou de simplement laisser la fonction se terminer naturellement.
Exemple :
def mon_generateur():
yield 1
yield 2
return "Fin"
yield 3 # Ce yield ne sera jamais atteint
g = mon_generateur()
print(next(g)) # 1
print(next(g)) # 2
try:
print(next(g))
except StopIteration as e:
print("Fin de l'itération, valeur :", e.value) # Fin de l'itération, valeur : Fin