Apprendre à utiliser les générateurs en Python 3

Gabor Laszlo Hajba

Traducteur

Laurent Rosenfeld

Ce tutoriel est une introduction aux générateurs en Python 3. Les générateurs sont des fonctions qui produisent des suites de résultats, et non une valeur unique.

Commentez

- Titre : Apprendre à utiliser les générateurs en Python 3

- Auteur : Gabor Laszlo Hajba

- Traducteur : Laurent Rosenfeld

- Parution : 11 décembre 2016

- Mise à jour : 11 décembre 2016

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Gabor Laszlo Hajba. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Générateurs

- III. Les fonctions génératrices

- IV. Expressions génératrices

- V. Exemples

- VI. Conclusion

- VII. Remerciements

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 :

1. def fibonacci(n):

2. if n <= 1:

3. return 1

4. return fibonacci(n-1) + fibonacci(n-2)

6. def generate_fibonacci(n):

7. fibonacci_numbers = []

8. for i in range(n):

9. fibonacci_numbers.append(fibonacci(i))

10. 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 :

1. def is_prime(n):

2. for i in range(2, n):

3. if n % i == 0:

4. return False

5. return True

7. def generate_primes(n):

8. primes = [2, 3, 5]

9. num = 6

10. while len(primes) < n:

11. if is_prime(num):

12. primes.append(num)

13. num += 1

14. return primes

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 :

1. def generate_fibonacci(n):

2. for i in range(n):

3. yield fibonacci(i)

5. def generate_primes(n):

6. primes = 0

7. num = 2

8. while primes < n:

9. if is_prime(num):

10. primes += 1

11. yield num

12. 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 :

1. def generate_fibonacci():

2. i = 0

3. while True:

4. yield fibonacci(i)

5. i += 1

7. def generate_primes():

8. num = 2

9. while True:

10. if is_prime(num):

11. yield num

12. 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.

1. 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 :

1. 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 :

1. sum_numbers = 0

2. for i in generate_fibonacci(40):

3. if 10000 < i:

4. break

5. sum_numbers += i

6. print(sum_numbers)

8. sum_primes = 0

9. for i in generate_primes(5000):

10. if 10000 < i:

11. break

12. sum_primes += i

13. print(sum_primes)

Voici le résultat des programmes d'origine (sans générateur) sur mon ordinateur :

1. >>> python3 fibonacci.py

2. 17710

3. duration: 50.830469369888306 seconds

5. >>> python3 primes.py

6. 5736396

7. duration:  10.034490585327148 seconds

Si je lance maintenant les programmes modifiés utilisant des générateurs sur le même ordinateur, j'obtiens :

1. >>> python3 fibonacci.py

2. 17710

3. duration:  0.0 seconds

5. >>> python3 primes.py

6. 5736396

7. 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.