Py: yield & générateurs
Py: yield & générateurs

Mot clé yield et les générateurs en Python

Directory

sources

Traduction

yield $ \Rightarrow $ transitive verb (conjugaison)

  1. 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
  2. relinquish, give up
    • céder,
    • abandonner
      to yield ground military (figurative) $ \Rightarrow $ céder du terrain
  3. (US) cars
    to yield right of way $ \Rightarrow $ céder la priorité

yield $ \Rightarrow $ noun

  1. 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
  2. finance
    • from investments rapport m, rendement m
    • profit bénéfice m, bénéfices mpl
    • from 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 forin 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 rencontre yield. 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