• 4 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 02/05/2018

Faites connaissance avec les générateurs et les itérateurs

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Vous êtes vous déjà demandé ce qu'il se passait lorsque vous écriviez une boucle for?

>>> for i in [0, 1, 2, 3]:
...    print(i)

C'est assez intuitif, nous avons une liste avec 4 éléments, et l'instruction for assigne à la variable i chacun de ses éléments un à un. Ensuite, nous faisons ce que nous souhaitons avec i : ici, on se contente de l'afficher.

Oui mais, l'instruction produit le même résultat que ce code :

>>> for i in range(4):
...    print(i)

Cependant, l'objet renvoyé par range(4) n'est pas une liste, c'est un objet de type... range ! A vrai dire, ce type d'objet est plutôt énigmatique : nous ne savons ni ce que c'est, ni ce qu'il y a dedans.

>>> range(4)
range(0, 4)

Comment donc l'instruction for  peut-elle déterminer les différents éléments qui composent un objet de type range?

La réponse tient en un mot : les itérables !

Les itérateurs

En fait, tous les objets de type itérables peuvent être itérés par un   for. Les listes, mais aussi les chaînes de caractères, les dictionnaires, etc.

Comment for interagit-il avec les itérateurs?

Que se passe-t-il lorsque j'écris cela?

for element in iterable_object:
    [...]

Comme   iterable_object  se trouve dans un for, Python va tout d'abord appeler cette instruction :

iter(iterable_object)

Celle-ci aura pour effet de retourner un objet de type itérateur, mais à condition que iterable_object soit un objet itérable. Bah oui, on n'itère pas sur un objet non itérable, c'est comme ça !

Par exemple, en écrivant for element in [1, 2, 3, 4], Python appellera l'instruction iter([1, 2, 3, 4]), qui renvoie ceci :

>>> it = iter([1, 2, 3, 4])
>>> it
<list_iterator at 0x7f712a7e3cc0>

Ensuite, Python va appeler l'instruction suivante autant de fois que possible :

next(it)

On tente ?

>>> iterable_object = [1, 2, 3, 4]
>>> iterator = iter(iterable_object)
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Ouh là ! Mais ce code a levé une exception ! Qu'a-ton fait de mal?

Pas de panique, nous n'avons rien fait de mal.

Pour la boucle for, il n'est pas toujours possible de déterminer combien d'éléments contient un objet itérable. Alors elle nous demande de la mettre au courant, et c'est bien normal ! Pour cela, l'itérateur lève une exception de type StopIteration lorsqu'il n'a plus d'objets à renvoyer. Lorsque for attrape cette exception, il sait qu'il faut s’arrêter.

Créer son propre itérateur

Puis-je créer mon propre itérateur à moi tout seul ?

Oui ! Il suffit de créer un objet qui implémente les 2 méthodes spéciales appelées par Python lorsqu'il rencontre une boucle for : __iter__ et __next__.

Par exemple :

class MyIterator:
    def __init__(self):
        print("Je m'initialise à 40")
        self.i = 40

    def __iter__(self):
        print("On a appelé __iter__")
        return self

    def __next__(self):
        print("On a appelé __next__")
        self.i += 2
        if self.i > 56 :
            raise StopIteration()
        return self.i

Ici, nous avons créé un itérateur qui renverra successivement les nombres de 40 (non compris) à 56, en comptant de deux en deux.MyIteratorinitialise son attribut self.i à 40. A chaque appel de __next__, il incrémente son attribut self.i de 2, et il garde intérieurement son "état" (représenté par self.i). Testons !

>>> for i in MyIterator():
...    print(i)
Je m'initialise à 40
On a appelé __iter__
On a appelé __next__
42
On a appelé __next__
44
On a appelé __next__
46
On a appelé __next__
48
On a appelé __next__
50
On a appelé __next__
52
On a appelé __next__
54
On a appelé __next__
56
On a appelé __next__

Itérons sur les députés !

Si nous revenons à notre projet, il est tout à fait possible de transformer notre objet  SetOfParliamentMember en itérateur. Il suffit de faire cela :

class SetOfParliamentMembers:

    ...

    def __iter__(self):
        self.iterator_state = 0
        return self

    def __next__(self):
        if self.iterator_state >= len(self):
            raise StopIteration()
        result = self[self.iterator_state]
        self.iterator_state += 1
        return result

Nous pouvons ici afficher les noms et adresses mail de tous les députés un à un :

>>> sopm = SetOfParliamentMembers("All MPs")
>>> sopm.data_from_csv(os.path.join("data","current_mps.csv"))
>>> for mp in sopm:
...    print(mp["nom"], mp["emails"])

Finalement, l'objet  range, c'est quoi ?

Vous l'aurez deviné, l'objet renvoyé par l'instruction  range(4)  que nous avons vu au tout début de ce chapitre, c'est un itérable ! Vous pouvez donc très bien l'utiliser dans une boucle  for, ou via  next  et  iter!

Les générateurs

 En Python, les générateurs ont été créés afin de simplifier la création d'itérateurs, et utilisent un mot magique : yield. Ce dernier est souvent difficile à appréhender, mais arrivé à ce stade, vous avez les neurones bien échauffés : vous allez y arriver !

Réécrivons MyIteratorsous forme de générateur. Nous l’appellerons my_generator.

def my_generator():
    i = 40
    while i <= 56:
        i += 2
        yield i

Mais ce que tu viens d'écrire, c'est une fonction !

Oui, c'est une fonction. Une fonction génératrice, qui renvoie un générateur. En fait, dès qu'une fonction contient le mot clé yield, elle retourne un générateur. Le terme générateur est à la fois employé pour désigner la fonction elle même (ici my_generator) et pour désigner le générateur que celle-ci retourne.

Pour tester ce générateur, utilisez le même code que pour testerMyIterator, et vous verrez que le résultat est exactement le même. Sauf qu'ici, nous avons écrit beaucoup moins de lignes de code.

Que se passe t-il lors de l’exécution de my_generator?

Quand une fonction génératrice est appelée, elle retourne uniquement un générateur, sans exécuter les instructions de la fonction. C'est seulement lorsque next est appelée que les instructions sont exécutées jusqu'à ce que le mot clé yield soit rencontré. A ce moment, la valeur spécifiée par yield est retournée. Ensuite, l'exécution est stoppée, mais l'état interne de la fonction génératrice (c'est à dire ses variables) est conservé. Lors d'un nouvel appel de next, l'exécution des instructions reprend là où elle s'était arrêtée, jusqu'à rencontrer un prochain yield. Si aucun yield n'est rencontré avant la fin de la fonction (ou si le mot clé return est rencontré), alors la fonction génératrice lève la StopIteration.

Un petit exemple : prenons un générateur qui renverra tous les nombre entiers compris entre deux nombres passés en paramètre, en alternant le résultat sous forme de float si le résultat est pair, puis sous forme de str si le résultat est impair. Enfin, il affichera quelques chaînes de caractères.

def generator(beginning, end):  
    print("    On commence !")

    cpt = beginning
    while cpt <= end:
        if beginning % 2 == 0:
            print("    On s'arrete au yield")
            yield float(cpt)
            print("    On reprend après le yield")
        else:
            print("    On s'arrete au yield")
            yield str(cpt)
            print("    On reprend après le yield")
        cpt += 1
    yield "C'est bientôt la fin"
    yield "C'est VRAIMENT bientôt la fin"
    yield "Là c'est la fin"
>>> for i in generator(4, 8):
...    print(i)
        On commence !
        On s'arrete au yield
    4.0
        On reprend après le yield
        On s'arrete au yield
    5.0
        On reprend après le yield
        On s'arrete au yield
    6.0
        On reprend après le yield
        On s'arrete au yield
    7.0
        On reprend après le yield
        On s'arrete au yield
    8.0
        On reprend après le yield
    C'est bientôt la fin
    C'est VRAIMENT bientôt la fin
    Là c'est la fin

Les expressions génératrices

Vous vous souvenez des compréhensions de listes?

>>> [2*x for x in range(3)]
[0, 2, 4]

Et bien je vous présente leur équivalent en générateur : les expressions génératrices ! C'est exactement le même principe, sauf qu'au lieu de renvoyer une liste, une expression génératrice renvoie un générateur :

>>> gen = (2*x for x in range(3))
>>> gen
<generator object <genexpr> at 0x7fa777055e60>

>>> sum(gen)
6

Vous venez de le voir : il suffit de remplacer les crochets par des parenthèses.

Aller plus loin : L'intérêt des générateurs

Si vous avez intuitivement compris l'intérêt des générateurs, vous pouvez passer cette partie. Je vais vous expliquer ici en quoi les générateurs peuvent nous faire gagner en mémoire et en lisibilité de code.

Prenons un exemple :

Un de vos amis a besoin de créer une fonction python qui filtre dans un texte tous les mots ayant au moins 6 caractères et qui contiennent la lettre "A". La taille du texte est potentiellement très grosse : imaginez par exemple que vous deviez exécuter cette fonction tous les jours (à heure fixe) sur l'ensemble des textes publiés sur facebook par l'ensemble des utilisateurs de ce réseau social ! Cela représente des centaines de milliers de mots chaque jour !

Vous décidez de relever le défi, et vous vous répartissez le travail : vous vous occupez de créer une fonction qui lit une très grosse chaîne de caractères (l'ensemble des posts facebook des dernières 24h) et la découpe en une liste de mots. Votre ami, de son côté, s'occupera des fonctions qui filtreront ces mots (mots contenant au moins 6 caractères avec la lettre A).

Vous créez donc la fonctionget_wordset lui les fonctionsfilter_by_sizeetfilter_by_letters.

Voici tout d'abord la donnée à traiter. Elle est ici petite, mais c'est pour tester !

big_data = """Le sénateur, dont il a été parlé plus haut, était un homme entendu qui 
    avait fait son chemin avec une rectitude inattentive à toutes ces rencontres qui font 
    obstacle et qu'on nomme conscience, foi jurée, justice, devoir; il avait marché droit à 
    son but et sans broncher une seule fois dans la ligne de son avancement et de son intérêt. 
    C'était un ancien procureur, attendri par le succès, pas méchant homme du tout, rendant 
    tous les petits services qu'il pouvait à ses fils, à ses gendres, à ses parents, même à 
    des amis; ayant sagement pris de la vie les bons côtés, les bonnes occasions, les bonnes 
    aubaines. Le reste lui semblait assez bête. Il était spirituel, et juste assez lettré 
    pour se croire un disciple d'Epicure en n'étant peut-être qu'un produit de Pigault-Lebrun.
    [...]
    (Les Misérables, Victor Hugo)
    """

Voici maintenant votre code :

import re

def is_part_of_a_word(character):
    return len(re.findall('\w', character, flags = re.UNICODE))  

def get_words(text):
    print("Je commence à lire le texte maintenant")
    
    words = []
    current_word = ""
    for character in text:
        if not is_part_of_a_word(character):
            if current_word != "":
                words += [current_word]
                current_word = ""
        else:
            current_word += character
    return words

Et voici le code de votre ami :

def filter_by_size(words):
    return [w for w in words if len(w) >= 6]

def filter_by_letters(words):
    return [w for w in words if "a" in w]
            

words = get_words(big_data)
print("Nombre de mots: %i" % len(words))
words = filter_by_size(words)
print("Nombre de mots: %i" % len(words))
words = filter_by_letters(words)
print("Nombre de mots: %i" % len(words))
        
print(words)

Ce qui donne ce résultat :

Je commence à lire le texte maintenant
Nombre de mots: 146
Nombre de mots: 42
Nombre de mots: 17
['sénateur', 'inattentive', 'obstacle', 'marché', 'avancement', 'ancien', 'attendri', 'méchant', 'rendant', 'pouvait', 'parents', 'sagement', 'occasions', 'aubaines', 'semblait', 'Pigault', 'Misérables']
def get_words(text):
    print("Je commence à lire le texte maintenant")
    
    words = []
    current_word = ""
    for character in text:
        if not is_part_of_a_word(character):
            if current_word != "":

                # code ajouté :
                if len(current_word) >= 6:
                    if "a" in current_word:
                        # fin du code ajouté
                        
                        words += [current_word]
                current_word = ""
        else:
            current_word += character
    return words

words = get_words(big_data)
print(words)

C'est une bonne solution, mais qui a deux inconvénients :

  1. Votre ami n'aime pas modifier ce que vous avez fait, il préfère que chacun travaille de son côté, car comme il n'est pas "dans votre tête", il a peur d'introduire des erreurs en modifiant votre code.

  2. Cela ajoute de la complexité visuelle au code : ici, on a rajouté 2 niveaux d'indentation ! (lignes 12 et 13)

Voici donc la meilleure des solutions, qui utilise des générateurs et des expressions génératrices :

def get_words(text):
    print("Je commence à lire le texte maintenant")
    
    current_word = ""
    for character in text:
        if not is_part_of_a_word(character):
            if current_word != "":
                yield current_word
                current_word = ""
        else:
            current_word += character

def filter_by_size(words):
    return (w for w in words if len(w) >= 6)

def filter_by_letters(words):
    return (w for w in words if "a" in w)
            
words = get_words(big_data)
words = filter_by_size(words)
words = filter_by_letters(words)
print("'words' est encore un générateur. Le texte n'a toujours pas été lu")
        
print("L'opération suivante va lancer la lecture du texte: ")
[w for w in words]

Elle est beaucoup plus lisible n'est-ce pas ?

Prenez 5 minutes pour bien comprendre l’enchaînement des opérations à l'aide desprint. Voici le résultat affiché :

'words' est encore un générateur. Le texte n'a toujours pas été lu
L'opération suivante va lancer la lecture du texte: 
Je commence à lire le texte maintenant

['sénateur',
 'inattentive',
 'obstacle',
 'marché',
 'avancement',
 'ancien',
 'attendri',
 'méchant',
 'rendant',
 'pouvait',
 'parents',
 'sagement',
 'occasions',
 'aubaines',
 'semblait',
 'Pigault',
 'Misérables']

Code du chapitre

Retrouvez le code de ce chapitre en cliquant ici. 

Exemple de certificat de réussite
Exemple de certificat de réussite