4.4. Les générateurs¶
Les générateurs sont un moyen simple de créer des itérateurs, sans devoir passer par l’écriture de classes.
Un générateur est juste une fonction, mais dont les valeurs de retour sont renvoyées séquentiellement comme dans le cas d’un itérateur.
Pour ce faire, il faut avoir recours à l’instruction yield
à la place de return
.
Du moment qu’une fonction comporte au moins une instruction yield
, elle est considérée comme un générateur.
Les méthodes __iter__()
et __next__()
sont automatiquement mises en œuvre pour les générateurs.
Note
Les fonctions sont en réalité des objets qui ont la propriété de pouvoir être appelés, c’est-à-dire dont la méthode spéciale __call__()
a été mise en œuvre.
Cette méthode est exécutée lorsque nous faisons suivre le nom d’un objet d’une paire de parenthèses.
Il est donc possible de créer ses propres objets appelables de cette manière.
L’instruction yield
renvoie une valeur, comme return
, mais elle met le déroulement de la fonction en pause au lieu de l’interrompre complètement.
Lorsque l’on appelle la fonction, elle renvoie l’itérateur permettant d’itérer sur ses valeurs de retour.
Lors de chaque itération, la fonction s’exécute jusqu’à ce qu’elle rencontre une instruction yield
ou se termine.
Dans le premier cas une valeur est renvoyée, dans le second l’exception StopIteration
est levée.
Le code ci-dessous illustre le principe des générateurs.
def generator():
yield 1
yield 2
yield 4
print("Générateur avec boucle for :")
for n in generator():
print(n)
print("Générateur avec appel à next() :")
it = generator()
print(next(it))
print(next(it))
print(next(it))
print(next(it))
Générateur avec boucle for :
1
2
4
Générateur avec appel à next() :
1
2
4
Traceback (most recent call last):
File "<input>", line...
print(next(it))
StopIteration
Puisque les générateurs sont mis en pause et ne se terminent pas avec yield
, toutes les valeurs des variables internes sont gardées en mémoire entre deux itérations.
Ainsi, il est par exemple possible de créer un générateur de puissances qui maintient son état interne entre deux appels à next()
.
def power_generator(base):
n = 0
while True:
yield base ** n
n += 1
it = power_generator(2)
for _ in range(5):
print(next(it))
1
2
4
8
16
4.4.1. Générateur de nombres premiers¶
Nous avons à présent toutes les connaissances pour réécrire proprement le premier programme de ce chapitre qui calculait la somme des 100’000 plus petits nombres premiers. Nous pouvons cette fois les obtenir à la volée à l’aide d’un générateur, afin de ne pas avoir à mémoriser la liste entière au préalable.
import math
def is_prime(n):
for d in range(2, int(math.sqrt(n)) + 1):
if n % d == 0:
return False
return True
def n_first_primes_generator(n):
primes_found = 0
x = 1
while primes_found < n:
if is_prime(x):
yield x
primes_found += 1
x += 1
total = 0
for n in n_first_primes_generator(100_000):
total += n
print(total)
Bien qu’il n’y ait pas de gain significatif en temps d’exécution, le programme est désormais plus élégant et bien plus économe en termes de ressources mémoire !
4.4.2. Générer la suite de Fibonacci¶
Comme exemple supplémentaire, considérons le problème du calcul de la suite de Fibonacci. Avec les générateurs, il devient aisé de produire à la volée chacun des termes d’une telle suite de manière simple et élégante, et sans devoir nécessairement les garder en mémoire.
def fib(n):
x, y = 0, 1
for _ in range(n):
yield x
x, y = y, x + y
for n in fib(9):
print(n)
0
1
1
2
3
5
8
13
21