Dans ce didacticiel, vous apprendrez à créer facilement des itérations à l'aide de générateurs Python, en quoi il est différent des itérateurs et des fonctions normales, et pourquoi vous devriez l'utiliser.
Vidéo: Générateurs Python
Générateurs en Python
Il y a beaucoup de travail dans la construction d'un itérateur en Python. Nous devons implémenter une classe avec une méthode __iter__()
et __next__()
, garder une trace des états internes et augmenter StopIteration
lorsqu'il n'y a aucune valeur à retourner.
C'est à la fois long et contre-intuitif. Generator vient à la rescousse dans de telles situations.
Les générateurs Python sont un moyen simple de créer des itérateurs. Tout le travail que nous avons mentionné ci-dessus est automatiquement géré par des générateurs en Python.
Pour parler simplement, un générateur est une fonction qui renvoie un objet (itérateur) sur lequel on peut itérer (une valeur à la fois).
Créer des générateurs en Python
Il est assez simple de créer un générateur en Python. C'est aussi simple que de définir une fonction normale, mais avec une yield
instruction au lieu d'une return
instruction.
Si une fonction contient au moins une yield
instruction (elle peut en contenir d'autres yield
ou des return
instructions), elle devient une fonction génératrice. Les deux yield
et return
renverront une valeur d'une fonction.
La différence est que si une return
instruction termine entièrement une fonction, l' yield
instruction met la fonction en pause en sauvegardant tous ses états et continue ensuite à partir de là lors d'appels successifs.
Différences entre la fonction du générateur et la fonction normale
Voici en quoi une fonction de générateur diffère d'une fonction normale.
- La fonction Generator contient une ou plusieurs
yield
instructions. - Lorsqu'il est appelé, il renvoie un objet (itérateur) mais ne démarre pas immédiatement l'exécution.
- Des méthodes comme
__iter__()
et__next__()
sont implémentées automatiquement. Nous pouvons donc parcourir les éléments en utilisantnext()
. - Une fois que la fonction cède, la fonction est mise en pause et le contrôle est transféré à l'appelant.
- Les variables locales et leurs états sont mémorisés entre les appels successifs.
- Enfin, lorsque la fonction se termine,
StopIteration
est déclenchée automatiquement lors des appels ultérieurs.
Voici un exemple pour illustrer tous les points mentionnés ci-dessus. Nous avons une fonction de générateur nommée my_gen()
avec plusieurs yield
instructions.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n
Une exécution interactive dans l'interpréteur est donnée ci-dessous. Exécutez-les dans le shell Python pour voir la sortie.
>>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration
Une chose intéressante à noter dans l'exemple ci-dessus est que la valeur de la variable n est mémorisée entre chaque appel.
Contrairement aux fonctions normales, les variables locales ne sont pas détruites lorsque la fonction cède. De plus, l'objet générateur ne peut être itéré qu'une seule fois.
Pour redémarrer le processus, nous devons créer un autre objet générateur en utilisant quelque chose comme a = my_gen()
.
Une dernière chose à noter est que nous pouvons utiliser des générateurs avec des boucles for directement.
C'est parce qu'une for
boucle prend un itérateur et l'itère en utilisant next()
function. Il se termine automatiquement lorsqu'il StopIteration
est soulevé. Vérifiez ici pour savoir comment une boucle for est réellement implémentée en Python.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)
Lorsque vous exécutez le programme, la sortie sera:
Ceci est imprimé en premier 1 Ceci est imprimé en deuxième 2 Ceci est imprimé en dernier 3
Générateurs Python avec une boucle
L'exemple ci-dessus est moins utile et nous l'avons étudié juste pour avoir une idée de ce qui se passait en arrière-plan.
Normalement, les fonctions du générateur sont implémentées avec une boucle ayant une condition de terminaison appropriée.
Prenons un exemple de générateur qui inverse une chaîne.
def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)
Production
Olleh
Dans cet exemple, nous avons utilisé la range()
fonction pour obtenir l'index dans l'ordre inverse en utilisant la boucle for.
Remarque : cette fonction de générateur fonctionne non seulement avec des chaînes, mais également avec d'autres types d'itérables comme liste, tuple, etc.
Expression du générateur Python
Des générateurs simples peuvent être facilement créés à la volée en utilisant des expressions de générateur. Cela facilite la construction de générateurs.
Semblables aux fonctions lambda qui créent des fonctions anonymes, les expressions génératrices créent des fonctions génératrices anonymes.
La syntaxe de l'expression du générateur est similaire à celle d'une compréhension de liste en Python. Mais les crochets sont remplacés par des parenthèses rondes.
La principale différence entre une compréhension de liste et une expression de générateur est qu'une compréhension de liste produit la liste entière tandis que l'expression de générateur produit un élément à la fois.
They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.
# Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)
Output
(1, 9, 36, 100)
We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.
Here is how we can start getting items from the generator:
# Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)
When we run the above program, we get the following output:
1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration
Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
Use of Python Generators
There are several reasons that make generators a powerful implementation.
1. Easy to Implement
Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.
class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result
The above program was lengthy and confusing. Now, let's do the same using a generator function.
def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1
Since generators keep track of details automatically, the implementation was concise and much cleaner.
2. Memory Efficient
A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.
Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.
3. Represent Infinite Stream
Les générateurs sont d'excellents supports pour représenter un flux infini de données. Les flux infinis ne peuvent pas être stockés en mémoire, et comme les générateurs ne produisent qu'un seul élément à la fois, ils peuvent représenter un flux infini de données.
La fonction de générateur suivante peut générer tous les nombres pairs (du moins en théorie).
def all_even(): n = 0 while True: yield n n += 2
4. Générateurs de pipelines
Plusieurs générateurs peuvent être utilisés pour canaliser une série d'opérations. Ceci est mieux illustré à l'aide d'un exemple.
Supposons que nous ayons un générateur qui produit les nombres de la série de Fibonacci. Et nous avons un autre générateur de quadrature des nombres.
Si nous voulons trouver la somme des carrés des nombres dans la série de Fibonacci, nous pouvons le faire de la manière suivante en pipelinant ensemble la sortie des fonctions génératrices.
def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))
Production
4895
Ce pipelining est efficace et facile à lire (et oui, beaucoup plus cool!).