Mot clé yield et les générateurs en Python
Directory
sources
Traduction
yield $ \Rightarrow $ transitive verb (conjugaison)
produce, bring in
- conjugaison produire
- conjugaison rapporter
- land, crops
produire, rapporter, donner
- results
donner - the investment bond will yield 11% $ \Rightarrow $ le bon d’épargne rapportera 11 %
- their research has yielded some interesting results $ \Rightarrow $ leur recherche a fourni OR donné quelques résultats intéressants
relinquish, give up
- céder,
- abandonner
to yield ground military (figurative) $ \Rightarrow $ céder du terrain
- (US) cars
to yield right of way $ \Rightarrow $ céder la priorité
yield $ \Rightarrow $ noun
- agriculture & industry
output
rendement m,- rapport m *
of crops
récolte f
- high-yield crops $ \Rightarrow $ récoltes à rendement élevé
- yield per acre $ \Rightarrow $ rendement à l’hectare
- finance
from investments
rapport m, rendement mprofit
bénéfice m, bénéfices mplfrom tax
recette f, rapport m
an 8% yield on investments $ \Rightarrow $ des investissements qui rapportent 8 %
Autres
- yield sign $ \Rightarrow $ panneau de priorité
- he yield himself up to the police $ \Rightarrow $ il s’est livré à la police
- to yield a secret $ \Rightarrow $ révéler un secret
Itérables
Lister un par un les éléments d’une liste $ \Rightarrow $ Itérer sur les éléments d’une liste
>>> l = [1, 2, 3]
>>> for i in l:
... print(i)
...
1
2
3
Avec une list comprehension, on crée une liste, donc un itérable. Avec une boucle for
, on opère sur ses élèments un par un $ \Rightarrow $ on itère dessus.
>>> l = [x for x in range(3)]
>>> for i in l:
... print(i)
...
0
1
2
À chaque fois qu’on peut utiliser for
…in
sur quelque chose, c’est un itérable: list, str, queue…
- pratique $ \Rightarrow $ on peut les lire autant qu’on veut
- tous les élèments sont stockés en mémoire $ \Rightarrow $ peut être un problème
Générateurs
La syntaxe d’une expression génératrice ressemble à une list comprehension à la différence qu’on utilise ()
à la place de []
. La grosse différence est qu’on ne peut pas lire un générateur plus d’une fois.
Le principe des générateurs c’est qu’ils génèrent tout à la volée et ne stock pas cette donnée en mémoire
def display(generateur):
print("start")
for i in generateur:
print(i)
print("end")
generateur = (x for x in range(3))
print("Premier appel de la fonction:")
display(generateur)
print("Second appel de la fonction:")
display(generateur)
output:
Premier appel de la fonction:
start
0
1
2
end
Second appel de la fonction:
start
end
On constate dans cet exemple qu’une fois que le générateur est utilisé une première fois, il ne contient plus rien pour le second appel.
Mot clé yield
Le mot clé yield
est utilisé en lieu et place de return
à la différence près qu’on va récupérer un générateur.
>>> def creerGenerateur():
... mylist = range(3)
... for i in mylist:
... yield i
...
>>> contient_generateur = creerGenerateur() # crée un générateur
>>> print(contient_generateur) # generateur est un objet
<generator object creerGenerateur at 0x7f69a52b3308>
>>> for i in contient_generateur:
... print(i)
0
1
2
TRES IMPORTANT !
Ce qu’il est important de comprendre dans cet exemple c’est que comme nous utilisons yield
et non pas return
, lors de l’appel de la fonction, le code de la fonction n’est pas exécuté et a la place, la fonction retourne un objet générateur.
- L’appel
creerGenerateur()
n’exécute pas le code dans le corp de sa définition (def creerGenerateur()
) -
creerGenerateur()
retourne un objet de type generator object -
Une fois dans la variable
contient_generateur
, tant qu’on ne l’utilise pas, il ne se passe rien ce n’est qu’une fois qu’on commence à itérer sur l’objet de type generator object que le code de la fonction s’exécute. -
La première fois que le code s’éxécute (cad une fois qu’on commence à itérer sur
contient_generateur
), il va partir du début de la fonction, arriver jusqu’àyield
et retrouner la première valeur. Ensuite, à chaque nouveau tour de boucle, le code va reprendre la où il s’est arrêté (Python sauvegarde l’état du code du générateur entre chaque appel), et exécute le code à nouveau jusqu’à ce qu’il rencontreyield
. Donc dans notre cas il va faire un tour de boucle. -
Il continue de cette façon jusqu’à ce que le code ne recontre plus de
yield
, et donc qu’il n’y a plus de valeur à retourner. Le générateur est alors considéré comme définitivement vide. Il ne peut pas être “rembobiné”, il faut en créer un autre. - La raison pour laquelle le code ne rencontre plus de
yield
est a déterminer nous même:- condition
- boucle
- recursion… voir yield à l’infini
Complément d’info sur l = range(x)
:
>>> l = range(3)
>>> print(l)
range(0, 3)
>>>
>>> print(type(l))
<class 'range'>
>>>
>>> for i in l:
... print(i)
...
0
1
2
En pratique
yield
permet d’économiser de la mémoire mais aussi de masquer la complexité d’un algo derrnière une API classique d’itération.
Une API est une interface de programmation qui permet de se « brancher » sur une application pour échanger des données.
Une fonction qui extrait les mots de plus de 3 caractères de tous les fichiers d’un dossier pourrait ressembler à ça:
import os
def extraire_mots(dossier):
for fichier in os.listdir(dossier):
with open(os.path.join(dossier, fichier)) as f:
for ligne in f:
for mot in ligne.split():
if len(mot) > 3:
yield mot
La complexité de cet algorithme est complèment masquée du point de vue de l’utilisateur qui peut l’utiliser de la façon suivante:
for mot in extraire_mots("/home/sol/Code/Python/Tuto/yield"):
print(mot)
De plus, l’utilisateur peut utiliser tous les outils qu’on utlise sur les itérables. Toutes les fonctions qui acceptent les itérables acceptent donc le résultat de la fonction en pramètre grâce au Duck Typing. On crée ainsi une sorte de boite à outils.
Controller yield
class DistributeurDeCapote():
stock = True
def allumer(self):
while self.stock:
yield "capote"
Tant qu’il y a du stock on peut récupérer autant de capotes que l’on veut.
>>> distributeur_en_bas_de_la_rue = DistributeurDeCapote()
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> print(next(distribuer))
capote
>>> print(next(distribuer))
capote
>>> print([next(distribuer) for c in range(4)])
['capote', 'capote', 'capote', 'capote']
Dès qu’il n’y a plus de stock…
>>> distributeur_en_bas_de_la_rue.stock = False
>>> print(next(distribuer))
Traceback (most recent call last):
File "/home/sol/Code/Python/Tuto/Class/04SamEtMax/yield/capotes.py", line 16, in <module>
print(next(distribuer))
StopIteration
Et c’est vrai pour tout nouveau générateur:
>>> distribuer2 = distributeur_en_bas_de_la_rue.allumer()
>>> print(next(distribuer2))
Traceback (most recent call last):
File "/home/sol/Code/Python/Tuto/Class/04SamEtMax/yield/capotes.py", line 19, in <module>
print(next(distribuer2))
StopIteration
Allumer une machine vide n’a jamais permis de remplir le stock. Mais il suffit de remplir le stock pour que ça refonctionne:
>>> distributeur_en_bas_de_la_rue.stock = True
>>> print(next(distribuer))
capote
>>> print(next(distribuer2))
capote
Des détails
En interne, tous les itérables utilisent un générateur appelé itérteur qu’on peut récupérer en utilisant la fonction iter()
.
>>> iter([1,2,3])
<list_iterator object at 0x7fca097fcba8>
>>> iter((1,2,3))
<tuple_iterator object at 0x7fca097fcbe0>
>>> iter(x for x in (1,2,3))
<generator object <genexpr> at 0x7fca097f3ca8>
La methode next()
avec l’iterable en argument retourne une valeur pour chaque appel de la méthode. Quand il n’y a plus de valeur, ils lèvent l’exception StopIteration
:
>>> gen = iter([1,2,3])
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
C’est de cette façon que les boucles for
fonctionnent. Elles utilisent iter()
pour créer un générateur, puis attrapent une exception pour s’arrêter.
À chaque boucle
for
, nous levons une exception sans le savoir !
L’implémentation actuelle est que iter()
appelle la méthode __iter__()
sur l’objet passé en paramètre. Donc ça veut dire qu’on peut créér nos propres itérables:
>>> class MonIterable(object):
... def __iter__(self):
... yield 'une'
... yield 'petite'
... yield 'poule'
...
>>> generateur = iter(MonIterable())
>>> print(next(generateur))
une
>>> print(next(generateur))
petite
>>> print(next(generateur))
poule
>>> print(next(generateur))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> for generateur2 in MonIterable():
... print(generateur2)
...
une
petite
poule