Un generator est une fonction ou une expression qui renvoie un type particulier d'itérateur appelé un generator iterator.
Les Generator-iterators sont paresseux : ils ne stockent pas leurs values en mémoire, mais génèrent leurs valeurs au moment où on en a besoin.
Une fonction génératrice ressemble à n'importe quelle autre fonction, mais elle 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).
Quand le générateur reprend son exécution, il repart de l'état où il s'était interrompu, contrairement aux fonctions classiques qui sont réinitialisées à chaque appel.
On construit les générateurs à peu près comme les autres fonctions itératives ou récursives, mais ils ont besoin d'une expression yield, que l'on verra plus en détail un peu plus loin.
Prenons l'exemple d'une fonction qui renvoie les carrés d'une liste donnée de nombres. Telle qu'elle est écrite actuellement, 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
L'idée derrière tout ça, c'est qu'on utilise un générateur quand on n'a pas besoin de produire toutes les valeurs d'un seul coup. Cela économise de la mémoire et de la puissance de calcul, puisque seule la valeur sur laquelle on travaille à ce moment-là est calculée.
On peut utiliser les générateurs à la place de la plupart des iterables en Python.
Cela inclut les fonctions ou les objets qui attendent un iterable/iterator en argument.
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 generator 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-itérateur, ce qui « fait avancer » le code ou l'évalue jusqu'à son expression yield, laquelle « produit » ou renvoie alors une valeur :
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
Quand un generator-iterator est entièrement consommé et 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-itérateurs sont un sous-ensemble particulier des itérateurs.
Les Iterators sont le mécanisme (ou protocole) qui permet de parcourir des iterables.
Les générateurs-ité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 à connaître :
indexes.
Tu ne peux pas référencer une valeur précédente ou suivante par addition ou 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 !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'elle soit demandée.
Quand yield est évalué, il met en pause l'exécution de la fonction qui le contient et renvoie les valeurs de cette 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é à 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 de nombreuses applications.
Quand on travaille avec une collection de valeurs potentiellement volumineuse, on n'a pas forcément envie de tout garder en mémoire. Un générateur permet de traiter des données plus importantes 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
Maintenant, chaque fois que __next__() est appelé sur l'objet infinite_sequence, il renvoie le nombre précédent + 1.
Conda Airlines est la plus grande compagnie aérienne du monde de la programmation, avec plus de 10 000 vols par jour !
Actuellement, ils attribuent tous les sièges aux passagers à la main ; il va falloir automatiser tout ça.
C'est à toi qu'ils ont demandé de créer un logiciel pour automatiser l'attribution des sièges aux passagers. Ils exigent que ton logiciel soit économe en mémoire et performant.
Conda veut générer les lettres de siège de ses avions.
Un avion est composé de rangées de sièges.
Chaque rangée compte 4 sièges.
Les sièges de chaque rangée portent toujours les noms A, B, C et D.
Le premier siège de la rangée est A, le deuxième est B, et ainsi de suite.
Après avoir atteint D, on repart à A.
Implémente une fonction generate_seat_letters(<number>) qui accepte un int indiquant le nombre de lettres de siège à générer.
La fonction doit ensuite renvoyer un iterable de lettres de siège.
>>> letters = generate_seat_letters(4)
>>> next(letters)
"A"
>>> next(letters)
"B"
Conda veut un système capable de générer un nombre donné de sièges pour ses avions.
Chaque avion a 4 sièges par rangée.
Les rangées sont définies par des nombres, en commençant à 1 et en augmentant.
Les sièges doivent être ordonnés, comme ceci : 1A, 1B, 1C, 1D, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, ...
Voici un exemple :
| x | 1 | 2 |
|---|---|---|
| Rangée | 5 | 21 |
| Lettre de siège | A | D |
| Résultat | 5A | 21D |
Beaucoup de compagnies aériennes n'ont pas de rangée numéro 13 sur leurs vols, par superstition chez les passagers. Conda Airlines suit également cette convention, alors assure-toi de ne pas générer de sièges pour la rangée numéro 13.
Implémente une fonction generate_seats(<number>) qui accepte un int indiquant le nombre de sièges à générer.
La fonction doit ensuite renvoyer un iterable des sièges demandés.
>>> seats = generate_seats(10)
>>> next(seats)
"1A"
>>> next(seats)
"1B"
Maintenant que tu as une fonction qui génère des sièges, tu peux t'en servir pour attribuer des sièges aux passagers.
Implémente une fonction assign_seats(<passengers>) qui accepte une list de noms de passagers.
La fonction doit ensuite renvoyer un dictionnaire avec passenger comme clé et seat_number comme valeur.
>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob']
>>> assign_seats(passengers)
{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'}
Conda Airlines aimerait avoir un code unique pour chaque billet. Comme c'est une grande compagnie aérienne, elle a beaucoup de vols. Cela signifie qu'il y a plusieurs vols avec le même numéro de siège. Ils veulent que tu crées un système qui génère un billet unique, une string de 12 caractères.
Ce code commence par assigned_seat, suivi de flight_id.
Le reste du code est rempli de 0s.
Implémente une fonction generate_codes(<seat_numbers>, <flight_id>) qui accepte une list de seat_numbers et une string contenant le numéro de vol.
La fonction doit ensuite renvoyer un generator qui produit un ticket_number.
>>> seat_numbers = ['1A', '17D']
>>> flight_id = 'CO1234'
>>> ticket_ids = generate_codes(seat_numbers, flight_id)
>>> next(ticket_ids)
'1ACO12340000'
>>> next(ticket_ids)
'17DCO1234000'
Inscris-toi sur Exercism pour apprendre et maîtriser Python avec 17 concepts146 exercices, et un vrai mentorat humain, le tout gratuitement.