Ce billet est le deuxième d'une série en quatre volets. Voir le volet 1, le volet 3 ou le volet 4.
Les générateurs sont une catégorie spéciale de fonction qui facilite l'écriture d'itérateurs.
Je vais me cantonner dans ce billet aux générateurs ou fonctions génératrices (les deux termes seront utilisés de manière interchangeable) dit simples au sens de la PEP 255.
Générateurs (ou fonctions génératrices)
En Python, une fonction est une fonction génératrice lorsque son corps contient le mot clé yield
.
def frange(start, stop, increment):
while start < stop:
yield start
start += increment
for n in frange(0, 4, 0.5):
print(n)
Lorsqu'elle est invoquée :
- ses arguments sont associés à son espace local, comme lors d'une invocation traditionnelle
- son code n'est pas exécuté, à la place un objet de type générateur-itérateur est retourné
- ce générateur-itérateur répond ensuite aux itérations et quand sa méthode
.__next__()
est invoquée, le corps de la fonction génératrice est exécuté jusqu'à ce qu'une instructionyield
produise un résultat et la mette en pause - au
.__next__()
suivant, l'exécution de la fonction génératrice reprend à l’endroit de la pause jusqu'auyield
suivant et ainsi de suite - le générateur-itérateur se termine quand une instruction
return
ou une exceptionStopIteration
est rencontrée, après quoi il n'est plus utilisable (il faudra ré-invoquer la fonction génératrice pour en produire un nouveau)
À partir du fonctionnement décrit ci-dessus on peut donner différentes définitions d'un générateur :
- c'est un type de fonction fabrique (
factory
) qui produit un générateur-itérateur (qui se conforme au protocole des itérateurs) - c'est un type de fonction qui produit une succession de résultats plutôt qu'un résultat unique
- c'est un type de fonction ayant la subtilité de pouvoir se mettre en pause en retournant un résultat intermédiaire à son appelant tout en maintenant son état local, de façon à pouvoir être reprise plus tard exactement à l'endroit où elle s'était arrêtée
Les générateurs peuvent être utilisés pour :
- simplifier l'écriture d'itérateurs, plus besoin de se soucier d'
.__iter__()
et de.__next__()
, voyez par exemple la différence entre un itérateur et une fonction génératrice - faire de l'évaluation paresseuse car chaque itération réutilise le même espace mémoire
- remplacer les callbacks, plutôt que de passer un callback à une fonction, un générateur peut céder le contrôle à l'appelant, ce dernier peut alors appliquer le traitement qu'aurait effectué le callback
Les présentations les plus connues sur les générateurs sont sans doute celles de David Beazley qui donne des exemples concrets d'utilisation. Dans le cadre des générateurs simples on peut aller lire Generator Tricks for Systems Programmers jusqu'à la partie 5 incluse.
Generator Expressions
Une Generator Expression est une syntaxe compacte pour écrire un générateur. La PEP 289 les présente comme une généralisation des List Comprehensions plus performante et plus économe en mémoire.
Cette notation est née du constat que dans la plupart des cas d'utilisation d'une List Comprehension il n'y a pas besoin d'avoir toute la liste en mémoire mais seulement le besoin d'itérer sur chacun des éléments un par un. C'est souvent considéré comme une best practice.
Les Generator Expressions sont particulièrement utiles avec des fonctions qui consomment un iterable et retournent une valeur unique (any()
, all()
, sum()
, min()
, max()
etc.). Lorsqu'une de ces fonctions n'a qu'un seul argument positionnel on peut lui passer une generator expression sans parenthèses supplémentaires :
sum(i for i in range(10000))
Une generator expression ne peut plus être réutilisée une fois consommée.
Ressources
- Generators — Python Functional Programming HOWTO
- Iterables vs. Iterators vs. Generators
- Chapter 4. Iterators and Generators — Python Cookbook
- Improve Your Python: `yield` and Generators Explained
- From List Comprehensions to Generator Expressions — The History of Python, Guido van Rossum