Un generator est une fonction ou une expression qui renvoie un type particulier d'itérateur appelé itérateur générateur.
Les Generator-iterators sont paresseux : ils ne stockent pas leurs values en mémoire, mais génèrent leurs valeurs quand c'est nécessaire.
Une fonction génératrice ressemble à n'importe quelle autre fonction, mais contient une ou plusieurs expressions yield.
Chaque yield suspend l'exécution du code et sauvegarde l'état d'exécution courant (y compris toutes les variables locales et les instructions try).
Lorsque le générateur reprend, il récupère l'état là où il s'était arrêté, contrairement aux fonctions classiques qui repartent de zéro à chaque appel.
Les générateurs se construisent un peu comme les autres fonctions à boucle ou récursives, mais ils nécessitent une expression yield, que l'on explorera plus en détail un peu plus loin.
Un exemple est une fonction qui renvoie les carrés d'un tableau de nombres donné.
Telle qu'elle est écrite, toutes les entrées doivent être traitées avant qu'une seule valeur puisse être renvoyée :
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
Tu peux transformer cette fonction en générateur comme ceci :
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
La raison est que l'on utilise un générateur quand on n'a pas besoin de toutes les valeurs d'un coup.
Cela économise de la mémoire et de la puissance de calcul, puisque seule la valeur sur laquelle on travaille à cet instant est calculée.
Les générateurs (techniquement des generator-iterators, voir la note ci-dessous.) sont un type d'iterator et peuvent être utilisés partout en Python là où un iterator ou un iterable est attendu.
Cela inclut les fonctions ou les objets qui nécessitent un iterable/iterator en argument.
Pour aller plus loin, voir Comment créer un itérateur en Python.
Les itérateurs générateurs sont un sous-ensemble particulier des itérateurs.
Les Iterators sont le mécanisme/protocole qui permet de parcourir des itérables.
Les itérateurs générateurs et les itérateurs renvoyés par les iterables courants de Python se comportent de façon très similaire, mais il y a quelques différences importantes à noter :
indexes.
Tu ne peux pas référencer une valeur précédente ou suivante en utilisant l'addition ou la soustraction, et tu ne peux pas utiliser la notation entre crochets ([]) ni le découpage.len(), car ils n'ont pas de longueur.generator-iterator infini !Pour utiliser le générateur squares_generator() :
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
Les valeurs d'un générateur peuvent aussi être produites ou récupérées via la fonction next().
next() appelle la méthode __next__() d'un objet générateur, ce qui fait « avancer » ou évaluer le code du générateur jusqu'à son expression yield, laquelle « produit » alors la valeur, autrement dit la renvoie.
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
Quand un generator est entièrement consommé et qu'il n'a plus de valeurs à renvoyer, il lève une erreur StopIteration.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Les générateurs sont un sous-ensemble particulier d'itérateurs.
Les Iterators sont le mécanisme/protocole qui permet de parcourir des itérables.
Les générateurs et les itérateurs renvoyés par les iterables courants de Python se comportent de façon très similaire, mais il y a quelques différences importantes à noter :
Les générateurs sont à sens unique ; il n'y a pas de « retour en arrière » vers une valeur précédente.
Parcourir un générateur consomme les valeurs renvoyées ; pas de réinitialisation.
Les générateurs (évalués paresseusement) ne peuvent pas être triés ni inversés.
Les générateurs n'ont pas d'indexes, donc tu ne peux pas référencer une valeur précédente ou suivante en utilisant l'addition ou la soustraction.
Les générateurs ne peuvent pas être utilisés avec la fonction len().
Les générateurs peuvent être finis ou infinis ; fais attention quand tu récupères toutes les valeurs d'un générateur infini.
yield
L'expression yield est très similaire à l'expression return.
Contrairement à l'expression return, yield transmet des valeurs à l'appelant à un moment précis, en suspendant l'évaluation ou le renvoi de toute valeur supplémentaire jusqu'à ce qu'elles soient demandées.
Quand yield est évalué, il met en pause l'exécution de la fonction qui l'englobe et renvoie les valeurs de la fonction à cet instant précis.
La fonction reste alors dans la portée, et quand __next__() est appelé, l'exécution reprend jusqu'à ce que yield soit rencontré de nouveau.
L'utilisation d'expressions yield est interdite en dehors des fonctions.
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
Les générateurs sont utiles dans beaucoup d'applications.
Quand on travaille avec une grande collection, on ne veut pas forcément mettre toutes ses valeurs en memory.
Un générateur permet de traiter des données volumineuses morceau par morceau, ce qui économise de la mémoire et améliore les performances.
Les générateurs sont aussi très utiles quand un processus ou un calcul est complexe, coûteux ou infini :
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
Désormais, chaque fois que __next__() est appelé sur l'objet infinite_sequence, il renvoie le nombre précédent + 1.