I. Introduction▲
De bons exemples d'utilisation des générateurs sont les calculs qui nécessitent beaucoup de temps processeur (en particulier pour des données en entrée volumineuses) et/ou les suites potentiellement infinies comme les nombres de Fibonacci ou les nombres premiers. Comment déterminez-vous le nombre maximal de nombres à générer ?
Voyons un exemple de base de génération de nombres de Fibonacci, sans utiliser de générateur pour l'instant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
def
fibonacci
(
n):
if
n <=
1
:
return
1
return
fibonacci
(
n-
1
) +
fibonacci
(
n-
2
)
def
generate_fibonacci
(
n):
fibonacci_numbers =
[]
for
i in
range(
n):
fibonacci_numbers.append
(
fibonacci
(
i))
return
fibonacci_numbers
L'exemple de code ci-dessus génère une liste de nombres de Fibonacci en appelant récursivement la fonction fibonacci. Si nous voulons afficher les dix premiers nombres de Fibonacci, nous pouvons écrire ceci à l'invite de l'interpréteur Python :
>>>
generate_fibonacci
(
10
)
[1
, 1
, 2
, 3
, 5
, 8
, 13
, 21
, 34
, 55
]
Si le nombre en paramètre passe à 40, cela va prendre quelque temps. Sur mon ordinateur, il a fallu 54 secondes pour générer les 40 premiers nombres de Fibonacci. Ce script souffre à l'évidence d'un goulot d'étranglement, mais nous pouvons nous en tenir à ce code dans le cadre du présent tutoriel.
Le second exemple effectue la génération de nombres premiers et la vérification de la primalité. Considérez le code suivant :
Si nous désirons générer les 10 premiers nombres premiers, nous pouvons appeler la fonction generate_primes comme suit :
>>>
generate_primes
(
10
)
[2
, 3
, 5
, 7
, 11
, 13
, 17
, 19
, 23
, 29
]
J'espère que vous vous rendez compte du problème : plus les nombres deviennent grands, plus les temps de réponse se dégradent. Dans les deux cas, il est en principe possible de générer une suite infinie de nombres.
II. Générateurs▲
Il est temps maintenant de nous pencher sur les générateurs. Quelle est la principale différence entre la solution ci-dessus et l'utilisation de générateurs ? Un générateur produit un résultat dès qu'il est disponible au lieu de renvoyer l'ensemble des résultats quand toutes les valeurs voulues sont disponibles. Ceci est dû au fait qu'un générateur renvoie un itérateur, un objet générateur. Cela ne va pas réduire le temps nécessaire à exécuter la fonction, mais vous obtenez les résultats à la demande. Par exemple, supposons que vous désirez faire la somme de tous les nombres premiers inférieurs à 10 000. Combien de nombres premiers allez-vous demander à la fonction de générer ? Comme vous ne savez pas à l'avance combien de nombres premiers il vous faut, vous allez demander un nombre suffisamment grand de nombres premiers, itérer sur le résultat et vous arrêter quand le nombre premier suivant est supérieur à 10 000. Mais nous avons vu précédemment que la génération de nombres premiers ralentit quand les nombres deviennent grands. Nous allons résoudre ce problème plus bas dans ce tutoriel.
Les générateurs présentent un autre avantage quand vous affichez les nombres à l'écran. La version d'origine du programme affiche tout quand il a fini. Les générateurs renvoient chaque élément à la demande. Vous pouvez donc afficher les nombres premiers à l'écran au fur et à mesure de leur génération. Il en va de même avec un itérateur : vous pouvez itérer sur les résultats au fur et à mesure de leur génération, il n'est pas nécessaire d'attendre que chaque élément ait été traité.
Les générateurs permettent d'itérer dans une boucle for … in opérant sur des intervalles ou tout autre objet itérable (listes, ensembles, maps). Ou vous pouvez les ajouter dans une boucle et appeler la fonction next() dans votre générateur.
III. Les fonctions génératrices▲
Les fonctions génératrices sont des fonctions qui renvoient un générateur. Les exemples ci-dessus renvoient une liste — ce n'est pas vraiment ce que nous voulons. Mais nous pouvons les transformer en fonctions génératrices en remplaçant les instructions return (dans les fonctions generate_*) par des instructions yield :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def
generate_fibonacci
(
n):
for
i in
range(
n):
yield
fibonacci
(
i)
def
generate_primes
(
n):
primes =
0
num =
2
while
primes <
n:
if
is_prime
(
num):
primes +=
1
yield
num
num +=
1
Mais cette solution est incomplète parce qu'il vous faut fournir une condition d'arrêt. J'ai mentionné plus haut que les générateurs sont la plupart du temps infinis — mais nous limitons les générateurs à un nombre maximal donné dans les exemples ci-dessus. Corrigeons cela et supprimons cette limitation :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def
generate_fibonacci
(
):
i =
0
while
True
:
yield
fibonacci
(
i)
i +=
1
def
generate_primes
(
):
num =
2
while
True
:
if
is_prime
(
num):
yield
num
num +=
1
Comme vous le voyez, j'ai simplement supprimé la liste contenant les résultats, éliminé le paramètre passé à la fonction et ajouté une boucle infinie pour poursuivre les calculs. Nous avons maintenant deux générateurs qui peuvent continuer à fournir des résultats aussi longtemps que nous en demandons.
IV. Expressions génératrices▲
Naturellement, il existe une façon pythonienne de créer des générateurs sans avoir besoin pour cela de passer par une fonction : vous pouvez aussi créer des expressions génératrices.
g =
(
p for
p in
generate_primes
(
))
Maintenant, la variable g contient une expression génératrice pour générer des nombres premiers. Il n'y a pas de liste créée (même pas en coulisses), la fonction generate_primes() n'est pas appelée.
Il est maintenant possible d'appeler la fonction next() pour obtenir le premier élément du générateur, exactement comme vous pouviez le faire avec la fonction génératrice :
print
(
next
(
g))
Ceci devrait afficher 2 à la console.
Remarque importante
Il est important de garder à l'esprit que quand vous commencez à utiliser un générateur et à lui demander un résultat, vous ne pouvez pas revenir en arrière sur les éléments générés. Il n'y a pas de fonction previous() qui vous renverrait le résultat précédent.
Pour réinitialiser un générateur, il faut le recréer. Il n'y a pas d'autre moyen.
V. Exemples▲
Pour bien voir la différence entre les deux solutions, modifions nos applications et calculons la somme des nombres (de Fibonacci et premiers) inférieurs ou égaux à 10 000 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
sum_numbers =
0
for
i in
generate_fibonacci
(
40
):
if
10000
<
i:
break
sum_numbers +=
i
print
(
sum_numbers)
sum_primes =
0
for
i in
generate_primes
(
5000
):
if
10000
<
i:
break
sum_primes +=
i
print
(
sum_primes)
Voici le résultat des programmes d'origine (sans générateur) sur mon ordinateur :
2.
3.
4.
5.
6.
7.
>>> python3 fibonacci.py
17710
duration: 50.830469369888306 seconds
>>> python3 primes.py
5736396
duration: 10.034490585327148 seconds
Si je lance maintenant les programmes modifiés utilisant des générateurs sur le même ordinateur, j'obtiens :
2.
3.
4.
5.
6.
7.
>>> python3 fibonacci.py
17710
duration: 0.0 seconds
>>> python3 primes.py
5736396
duration: 0.4524550437927246 seconds
Les résultats obtenus sont les mêmes, mais les durées d'exécution incomparablement meilleures, parce que les fonctions modifiées ne calculent pas tous les nombres de Fibonacci ou nombres premiers jusqu'à la valeur paramétrée, mais s'arrêtent dès que l'on atteint le seuil de 10 000.
VI. Conclusion▲
Nous avons examiné les générateurs à travers deux exemples qui montrent comment nous pouvons les mettre à profit et utiliser leur évaluation paresseuse pour différer les calculs gourmands en CPU jusqu'au moment où ils sont vraiment nécessaires.
Les générateurs sont utiles quand vous devez générer une longue liste (peut-être infinie) de valeurs alors que vous n'aurez peut-être pas besoin de les utiliser toutes.
VII. Remerciements▲
Nous remercions https://hahamo.wordpress.com/ de nous avoir autorisé à traduire et publier son tutoriel Generator in Python 3.
Nous tenons également à remercier Lolo78 pour la traduction, Deusyss pour la relecture technique et f-leb pour la relecture orthographique.