3.1 Introduction aux générateurs
Les générateurs sont des fonctions qui retournent un objet itérateur. Ces itérateurs génèrent des valeurs à la demande, ce qui permet de gérer potentiellement de grandes quantités de données sans les charger entièrement en mémoire.
Il existe plusieurs façons de créer des générateurs, et nous verrons ci-dessous les plus populaires.
Générateurs basés sur les fonctions
Les générateurs sont créés en utilisant le mot-clé yield à l'intérieur d'une fonction. Lorsque la fonction avec yield est appelée, elle retourne un objet générateur, mais n'exécute pas immédiatement le code à l'intérieur de la fonction. Au lieu de cela, l'exécution est suspendue à l'expression yield et reprend à chaque appel de la méthode __next__() de l'objet générateur.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # Sortie: 1
print(next(counter)) # Sortie: 2
print(next(counter)) # Sortie: 3
print(next(counter)) # Sortie: 4
print(next(counter)) # Sortie: 5
Si une fonction contient une instruction yield, Python crée un objet générateur qui gère l'état d'exécution de la fonction au lieu de l'exécuter traditionnellement.
Expressions génératrices
Les expressions génératrices ressemblent aux List Comprehensions, mais sont créées en utilisant des parenthèses au lieu de crochets. Elles retournent également un objet générateur.
squares = (x ** 2 for x in range(10))
print(next(squares)) # Sortie: 0
print(next(squares)) # Sortie: 1
print(next(squares)) # Sortie: 4
Lequel de ces moyens préfères-tu ?
3.2 Les avantages des générateurs
Utilisation efficace de la mémoire
Les générateurs calculent les valeurs à la volée, ce qui permet de traiter de grandes quantités de données sans les charger entièrement en mémoire. Cela rend les générateurs idéaux pour travailler avec de grands ensembles de données ou des flux de données.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# Traiter les valeurs une par une
print(value)
Calculs paresseux
Les générateurs effectuent des calculs paresseux, ce qui signifie qu'ils calculent les valeurs uniquement lorsqu'elles sont nécessaires. Cela permet d'éviter des calculs inutiles et d'améliorer les performances.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
Simplicité syntaxique
Les générateurs offrent une syntaxe pratique pour créer des itérateurs, ce qui simplifie l'écriture et la lecture du code.
3.3 Utilisation des générateurs
Exemples d'utilisation des générateurs dans la bibliothèque standard
De nombreuses fonctions dans la bibliothèque standard de Python utilisent des générateurs. Par exemple, la fonction range() retourne un objet générateur qui génère une séquence de nombres.
for i in range(10):
print(i)
Oui, le monde ne sera plus jamais le même.
Création de séquences infinies
Les générateurs permettent de créer des séquences infinies, utiles dans divers scénarios tels que la génération de flux de données infinis.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
Utilisation de send() et close()
Les objets générateurs supportent les méthodes send() et close(), qui permettent d'envoyer des valeurs à un générateur et de terminer son exécution.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # Lancer le générateur
e.send("Hello, world!") # Sortie: Hello, world!
e.close()
3.4 Générateurs en pratique
Générateurs et exceptions
Les générateurs peuvent gérer les exceptions, ce qui en fait un outil puissant pour écrire du code plus robuste.
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("Generator closed")
gen = controlled_execution()
print(next(gen)) # Sortie: Start
print(next(gen)) # Sortie: Working
gen.close() # Sortie: Generator closed
Nous verrons comment gérer les exceptions dans les prochains cours, mais je pense que ce serait utile de savoir que les générateurs fonctionnent bien avec elles.
Générateurs imbriqués
Les générateurs peuvent être imbriqués, ce qui permet de créer des structures itératives complexes.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# Sortie
0
1
2
A
B
C
Explication :
yield from : Cette construction est utilisée pour déléguer une partie des opérations à un autre générateur, ce qui permet de simplifier le code et d'améliorer la lisibilité.
Générateurs et performance
L'utilisation de générateurs peut considérablement améliorer la performance des programmes en réduisant l'utilisation de la mémoire et en rendant les itérations plus efficaces.
Exemple de comparaison entre listes et générateurs
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# Utilisation d'une liste
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# Utilisation d'un générateur
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # Calculer la somme pour comparer les résultats
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"Liste:")
print(f" Temps: {list_time:.2f} sec")
print(f" Mémoire: {list_memory:,} octets")
print(f"\nGénérateur:")
print(f" Temps: {gen_time:.2f} sec")
print(f" Mémoire: {gen_memory:,} octets")
Liste:
Temps: 0.62 sec
Mémoire: 89,095,160 octets
Générateur:
Temps: 1.13 sec
Mémoire: 200 octets
GO TO FULL VERSION