Les générateurs Python pour maîtriser votre code

Les générateurs Python sont un outil très puissant, basés sur les concepts d’itération et ils offrent des modèles qui combinent élégance avec efficacité.

Les générateurs Python sont des itérateurs, qui ne peuvent être parcouru qu’une seule fois. En fait, ils ne stockent pas toutes les valeurs en mémoire, ils génèrent les valeurs à la volée.
Vous pouvez les utiliser en les parcourant, soit avec une boucle for, soit en les passant à toute fonction ou une structure qui itère.

La plupart du temps, les générateurs sont implémentés en tant que fonctions. Cependant, au lieu de renvoyer une valeur avec return, ils utilisent le mot clé yield.

générateurs Python

La différence entre une fonction normal et une fonction génératrice

Au lieu de renvoyer les résultats en une seule fois, un générateur Python peut démarrer, donner une valeur, suspendre l’opération tout en conservant tout ce dont il a besoin pour pouvoir reprendre le calcul, s’il est rappelé, pour passer à l’étape suivante.

Les fonctions génératrices sont automatiquement transformés en leurs propres itérateurs par Python, ce qui vous permet de les appeler ensuite.

En fait, c’est un mécanisme très puissant. Voyons pourquoi ?
Supposons que je vous ai demandé de compter à voix haute de 1 à un million. Vous allez commencer, et à un moment donné, je vous demande d’arrêter.
Après un certain temps, je vous demande de reprendre. À ce stade, pour pouvoir reprendre, vous devez vous souvenir du nombre auquel vous vous êtes arrêté .
Si vous vous êtes arrêté à 1000, vous allez reprendre à partir de 1001.
Eh bien, c’est ainsi qu’un générateur ce comporte !

Prenons un exemple :

# une fonction classique qui renvoie le carré des éléments d'un range
def square(n): 
    return [x ** 2 for x in range(n)]
print(square(10))

L’exécution du code de la fonction classique retournera toutes les valeurs.

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Reprenons le même exemple avec un yield.

# un générateur qui renvoie le carré
def square_gen(n):
    for x in range(n):
        yield x ** 2 # yield remplace return
print(square_gen(10))

L’exécution du code :

<generator object square_gen at 0x7f0d819c5ac0>

Comme vous pouvez le constater, le générateur Python n’a renvoyé aucune valeur. Toutefois, un objet générateur à été créé.
Mais, qu’est ce qu’il va générer ?

Appliquons la fonction list() à notre générateur pour voir ce qu’il nous cache.

print(list(square_gen(10)))

Exécutez votre code.

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Et la magie de Python surgit!

Notre générateur est un objet itérable, sur lequel il est possible d’appliquer une boucle for.

for i in square_gen(10):
    print(i)

L’exécution du code.

0
1
4
9
16
25
36
49
64
81

Alors que return termine entièrement une fonction, yield met la fonction en mode pause jusqu’à ce qu’elle soit à nouveau appelée par la méthode next().

En fait, l’instruction yield arrête la fonction et enregistre l’état local afin qu’elle puisse être reprise là où elle s’était arrêtée. Les fonctions de générateur peuvent contenir une ou plusieurs yield.

J’espère que c’est clair jusqu’ici.
Puisque nous allons passer à autre chose.

Un générateur est aussi un itérateur, mais qu’est-ce qu’un itérateur ?

Qu’est ce qu’un itérable

Un itérable est tout objet en Python qui possède une méthode __iter__ qui retourne un itérateur ou peut prendre des index. En bref, un itérable est tout objet qui peut nous fournir un itérateur. La fonction iter() (qui à son tour appelle la méthode __iter__) renvoie un itérateur.

Alors, qu’est-ce qu’un itérateur ?

Un itérateur est tout objet en Python qui possède une méthode __next__. La fonction next() sert à parcourir tous les éléments d’un itérateur. Lorsque nous atteignons la fin et qu’il n’y ai plus de données, une erreur StopIteration est déclenchée.

En fait, c’est ce qui se passe automatiquement avec une boucle for en arrière plan. Il existe de nombreux types d’objets qui peuvent être utilisés avec une boucle for. On les appelle des objets itérables.

C’est abstrait, je sais. Prenons un exemple !
Exécutez ce code ligne par ligne, puis lisez les commentaires.

# créer une liste
ma_list = [1, 2, 3]
# __iter__ existe, ma_list est un itérable
# notez l'absence de __next__ 
print(dir(ma_list))    

print(next(ma_list))   #  TypeError: 'list' object is not an iterator

# créer l'itérateur en utilisant iter()
mon_iterateur = iter(ma_list)

# notez la présence de __next__
# l'itérateur a été créé
print(dir(mon_iterateur))

print(mon_iterateur)             # <list_iterator object at 0x7f2067f1dee0>
print(list(mon_iterateur))       # [1, 2, 3]
print(ma_list is mon_iterateur)  # False

En exécutant ce code, nous constatons que mon_itérateur et ma_liste sont deux objets différents.

continuons avec cette ligne de code.

print(next(mon_iterateur))

L’exécution du code :

1

Ajoutez ces lignes et exécutez le code :

print(next(mon_iterateur))
print(next(mon_iterateur))
print(next(mon_iterateur))

Ce qui nous donne.

1
2
3
Traceback (most recent call last):
  File "/monProjet/test.py", line 10, in <module>
    print(next(mon_iterateur))
StopIteration

StopIteration est retourné car il n’y a plus d’éléments.

Très bien, passons à un autre concept !

À quoi sert un générateur Python ?

Dans l’exemple précédent, le générateur n’est pas vraiment utile. En fait, les générateurs Python sont bien placés pour calculer un grands nombres de résultats (en particulier les calculs impliquant des boucles elles-mêmes) où vous ne souhaitez pas allouer de la mémoire pour tous les résultats en même temps.

De nombreuses fonctions de la bibliothèque standard qui renvoient des listes en Python 2 ont été modifiées pour renvoyer des générateurs en Python 3 car les générateurs nécessitent moins de ressources.

Reprenons notre exemple du générateur square_gen

# un générateur qui renvoie le carré d'un range
def square_gen(n):
    for x in range(n):
        yield x ** 2 # yield remplace return
gen = square_gen(4)

print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))    # cet appel va générer une erreur StopIteration

L’exécution du code :

0
1
4
9
Traceback (most recent call last):
  File "/monProjet/gen.py", line 11, in <module>
    print(next(gen))
StopIteration

Chaque fois que nous appelons next avec l’objet générateur, nous le démarrons ou le faisons reprendre à partir du dernier point atteint. Le premier appel de next, nous donne la valeur 0, qui est le carré de 0, puis 1, puis 4, puis 9 et puisque il n y a plus d’élément à itérer, la boucle for s’arrête après cela (n vaut 4), alors le générateur s’arrête automatiquement. En fait, dans un tel cas, une fonction classique renverrait simplement None, mais pour se conformer au protocole d’itération, un générateur Python lèvera une exception StopIteration.

Cela explique comment fonctionne une boucle for. Lorsque vous appelez k dans un range (n), en arrière plan, la boucle for utilise un itérateur et commence à parcourir les éléments, jusqu’à ce qu’une StopIteration soit déclenché, ce qui indique à la boucle for que l’itération a atteint sa fin.
L’intégration de ce comportement dans chaque aspect d’itération de Python rend les générateurs encore plus puissants, car une fois que nous les écrivons, nous serons en mesure de les déclencher avec le mécanisme d’itération de notre choix.

Créer un générateur Python pour la suite de Fibonacci

En mathématiques, la suite de Fibonacci est les nombres affichés dans la séquence suivante.

Suite de Fibonacci = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34…

Dans la suite de Fibonacci, la première valeur est 0, la deuxième valeur est 1 et le nombre suivant est le résultat de la somme des deux nombres précédents. Par exemple, la troisième valeur est (0 + 1), la quatrième valeur est (1 + 1) et ainsi de suite.

Donc, afin de créer un code Python pour cette suite, vous devez connaitre au moins les deux premiers nombres. Dans ce cas le 0 et le 1, ce qui vous permettra de calculer le reste de la suite.

En fait, nous allons créer un code Python qui permet de saisir n’importe quel entier positif. Ensuite, le programme affichera la suite de Fibonacci de 0 aux nombres spécifiés par l’utilisateur.

def fib(n):
    a = 0
    b = 1
    for i in range(n):
        print(a)
        c = a       # mémoriser la valeur de a avec une variable temporaire                
        a = b       # transmettre la valeur de b à a
        b = c + b   # le nouveau b et l'ancien a plus l'ancien b

Reprenons l’exemple, mais cette fois nous allons stocker le résultat dans une liste. Au lieu d’utiliser une variable temporaire pour déplacer les valeurs de a et b, nous allons profiter de l’une des propriétés des tuples en écrivant a, b = b, a + b. La valeur de b est affecté à la variable a et la valeur de a+b est affectée à la variable b. Nous créons une liste vide result qui stockera les valeurs de notre suite de Fibonacci. À chaque itération la nouvel valeur sera ajoutée à la liste avec la méthode append. À la fin nous récupérons notre suite grâce à un return.

def fib(n):
    a = 0
    b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result

Ce qui nous donne comme résultat.

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Supposons à présent que nous voulions exécuter ce code :

print(fib(1000000))

Ne le faites pas, votre machine sera bloquée!

Le code ci dessus épuisera toutes les ressources de votre PC en faisant un calcul si important. Cependant, la fonction next() est d’une grande utilité en accédant uniquement à l’élément suivant d’une séquence.
Créons d’abord notre générateur Python.

def fib_gen(n):
    a = 0
    b = 1
    for i in range(n):
        yield a
        a, b = b, a+b

gen = fib_gen(1000000)
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))

L’exécution du code.

0
1
1
2

Vous avez un générateur, que vous itérez manuellement en appelant à plusieurs reprises la fonction next(). C’est un outil excellent qui vous permet de contrôler le flux de votre code et s’assurer du rendement final.

Les expression de générateur

Python fournit également une expression de générateur qui est une façon plus courte pour définir de simples fonctions génératrices. L’expression de générateur est un générateur anonyme. Prenons l’exemple de notre fonction square().

square = (x*x for x in range(5))

print(next(square))
print(next(square))
print(next(square))

L’exécution du code.

0
1
4

On a déjà vue ce genre de déclaration dans les compréhension Python. La syntaxe est similaire à celle des compréhensions de liste, mais au lieu des crochets, elle utilise des parenthèses.

Le code (x*x for x in range(5)) est une expression de générateur. La première partie d’une expression est le résultat et la deuxième partie est la boucle for qui itère sur la séquence.


Les générateurs Python pour maîtriser votre code

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Retour en haut