Module : Itérateurs et Générateurs
1. Quoi : Le Protocole d'Itération
En Python, l'itération (le fait de parcourir une séquence avec une boucle for) est un protocole bien défini.
- Un objet itérable est un objet capable de retourner ses membres un par un. C'est tout objet sur lequel on peut faire une boucle
for(listes, tuples, chaînes, dictionnaires, etc.). Techniquement, c'est un objet qui a une méthode__iter__(). - Un itérateur est un objet qui représente un flux de données. Il se souvient de sa position dans le flux. Il a une méthode
__next__()qui retourne l'élément suivant et lève une exceptionStopIterationquand il n'y a plus d'éléments.
Quand vous faites for element in my_list:, Python appelle d'abord iter(my_list) pour obtenir un itérateur, puis appelle next() sur cet itérateur à chaque tour de boucle jusqu'à recevoir StopIteration.
2. Les Générateurs : Des Itérateurs Simplifiés
Créer une classe d'itérateur manuellement (avec __iter__ et __next__) est possible mais verbeux. Les générateurs offrent une manière beaucoup plus simple et élégante de créer des itérateurs.
Il y a deux types de générateurs :
- Les fonctions génératrices (utilisant le mot-clé
yield). - Les expressions génératrices (syntaxe similaire aux compréhensions de liste).
3. Les Fonctions Génératrices et yield
Une fonction génératrice est une fonction qui utilise le mot-clé yield au lieu de return.
- Quand une fonction génératrice est appelée, elle ne s'exécute pas. Elle retourne un objet générateur, qui est un type d'itérateur.
- Chaque fois que
next()est appelé sur le générateur, la fonction s'exécute jusqu'à ce qu'elle rencontre une instructionyield. yieldmet en pause l'exécution de la fonction, retourne la valeur spécifiée, et sauvegarde son état local (ses variables).- À l'appel suivant de
next(), la fonction reprend son exécution juste après leyield, avec son état intact.
Exemple : Un simple compteur
def simple_generator():
print("Generator starts")
yield 1
print("Generator resumes")
yield 2
print("Generator resumes again")
yield 3
print("Generator ends")
# 1. On obtient un objet générateur
gen = simple_generator()
print(type(gen)) # <class 'generator'>
# 2. Premier appel à next()
print(f"First value: {next(gen)}")
# Affiche:
# Generator starts
# First value: 1
# 3. Deuxième appel à next()
print(f"Second value: {next(gen)}")
# Affiche:
# Generator resumes
# Second value: 2
# 4. Troisième appel à next()
print(f"Third value: {next(gen)}")
# Affiche:
# Generator resumes again
# Third value: 3
# 5. Quatrième appel à next() -> StopIteration
# next(gen) # Lèverait une StopIteration
Une boucle for gère tout cela automatiquement.
for value in simple_generator():
print(f"Value from for loop: {value}")
4. Pourquoi utiliser des Générateurs ? La "Lazy Evaluation"
Le principal avantage des générateurs est la "lazy evaluation" (évaluation paresseuse). Ils ne génèrent les valeurs qu'au moment où on les demande.
- Efficacité en mémoire : C'est extrêmement efficace pour travailler avec de très grandes séquences de données. Une fonction qui retourne une liste d'un milliard d'éléments consommerait une quantité énorme de RAM. Un générateur qui produit ces mêmes éléments un par un n'en stocke qu'un seul en mémoire à la fois.
- Traitement de flux infinis : Les générateurs peuvent représenter des séquences infinies (ex: un générateur qui produit tous les nombres premiers, ou des données provenant d'un capteur en temps réel).
Exemple : Fichiers volumineux
Mauvais (charge tout en mémoire)
def read_file_bad(filename):
with open(filename, 'r') as f:
return f.readlines() # Retourne une liste de toutes les lignes
Bon (utilise un générateur)
def read_file_good(filename):
with open(filename, 'r') as f:
for line in f:
yield line # Retourne les lignes une par une
Avec la deuxième version, vous pouvez traiter un fichier de plusieurs gigaoctets sans jamais saturer votre mémoire.
5. Les Expressions Génératrices
Une expression génératrice a une syntaxe très similaire à une compréhension de liste, mais avec des parenthèses () au lieu de crochets [].
- Compréhension de liste :
[x*x for x in range(10)]- Crée une liste complète en mémoire.
- Expression génératrice :
(x*x for x in range(10))- Crée un objet générateur qui produira les valeurs à la demande.
# Crée une liste de 100 millions de nombres en mémoire
list_comp = [i for i in range(100_000_000)]
# Crée un objet générateur, ne consomme presque pas de mémoire
gen_expr = (i for i in range(100_000_000))
# On peut ensuite utiliser le générateur
total = sum(gen_expr) # sum() itère sur le générateur sans tout stocker
✅ Bonne pratique : Si vous n'avez besoin d'itérer sur le résultat qu'une seule fois (par exemple, pour le passer à une autre fonction comme sum() ou dans une boucle for), utilisez une expression génératrice. C'est plus efficace en mémoire.
Exercice 01 : Générateur de la Suite de Fibonacci
Objectif
Cet exercice a pour but de vous faire implémenter une fonction génératrice pour produire les nombres de la suite de Fibonacci, en illustrant l'avantage des générateurs pour créer des séquences potentiellement infinies avec une consommation mémoire minimale.
Contexte
La suite de Fibonacci est une suite de nombres où chaque nombre est la somme des deux précédents. Elle commence généralement par 0 et 1.
0, 1, 1, 2, 3, 5, 8, 13, 21, ...
Créer une liste de tous les nombres de Fibonacci jusqu'à une très grande limite pourrait consommer beaucoup de mémoire. Un générateur est donc la solution parfaite.
Énoncé
-
Créez un nouveau fichier Python nommé
fibonacci_generator.py. -
Définissez une fonction génératrice nommée
fibonacci_sequencequi accepte un argumentlimit.- Cette fonction générera les nombres de Fibonacci tant qu'ils sont inférieurs à
limit.
- Cette fonction générera les nombres de Fibonacci tant qu'ils sont inférieurs à
-
À l'intérieur de la fonction : a. Initialisez les deux premiers nombres de la suite, par exemple
a, b = 0, 1. b. Utilisez une bouclewhilequi continue tant que le nombre actuel (a) est inférieur àlimit. c. Dans la boucle : - Utilisezyield apour produire le nombre actuel de la suite. - Mettez à jour les variables pour calculer le nombre suivant. L'ancienbdevient le nouveaua, et la sommea + bdevient le nouveaub. - Astuce : L'assignation multiple en Python,a, b = b, a + b, est parfaite pour cela. -
Testez votre générateur :
- Créez une boucle
forpour itérer sur les nombres produits parfibonacci_sequence(100)et affichez chaque nombre. - Utilisez une expression génératrice et la fonction
sum()pour calculer la somme des nombres de Fibonacci inférieurs à 1000.
- Créez une boucle
Résultat Attendu
Fibonacci numbers up to 100:
0
1
1
2
3
5
8
13
21
34
55
89
Sum of Fibonacci numbers up to 1000: 2583
Cliquez ici pour voir un exemple de code de solution
# fibonacci_generator.py
def fibonacci_sequence(limit):
"""
A generator function that yields Fibonacci numbers up to a given limit.
"""
# Initialize the first two numbers
a, b = 0, 1
# Loop as long as the current number is less than the limit
while a < limit:
# Yield the current number, pausing execution
yield a
# Update the numbers for the next iteration
a, b = b, a + b
# --- Testing ---
# Test 1: Print Fibonacci numbers up to 100
print("Fibonacci numbers up to 100:")
# The for loop automatically handles the generator
for number in fibonacci_sequence(100):
print(number)
print("\n" + "-"*20 + "\n")
# Test 2: Sum of Fibonacci numbers up to 1000 using a generator expression
# This is equivalent to sum(fibonacci_sequence(1000))
fib_gen_expr = (n for n in fibonacci_sequence(1000))
total_sum = sum(fib_gen_expr)
print(f"Sum of Fibonacci numbers up to 1000: {total_sum}")