247 votes

Comprendre les générateurs en Python

Je suis en train de lire le livre de recettes Python et je me penche actuellement sur les générateurs. J'ai du mal à m'y retrouver.

Comme je viens d'un environnement Java, existe-t-il un équivalent Java ? Le livre parlait de "producteur/consommateur", mais quand j'entends cela, je pense à l'enfilage.

Qu'est-ce qu'un générateur et pourquoi l'utiliser ? Sans citer de livres, évidemment (sauf si vous pouvez trouver une réponse décente et simpliste directement dans un livre). Peut-être avec des exemples, si vous vous sentez généreux !

440voto

Stephan202 Points 27707

Remarque : ce billet suppose une syntaxe Python 3.x.

A générateur est simplement une fonction qui renvoie un objet sur lequel vous pouvez appeler next de telle sorte que, à chaque appel, il renvoie une valeur, jusqu'à ce qu'il déclenche un message d'erreur. StopIteration exception, signalant que toutes les valeurs ont été générées. Un tel objet est appelé un itérateur .

Les fonctions normales renvoient une valeur unique en utilisant return tout comme en Java. En Python, cependant, il existe une alternative, appelée yield . Utilisation de yield n'importe où dans une fonction en fait un générateur. Observez ce code :

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Comme vous pouvez le voir, myGen(n) est une fonction qui donne n y n + 1 . Chaque appel à next produit une seule valeur, jusqu'à ce que toutes les valeurs aient été produites. for appel de boucles next en arrière-plan, donc :

>>> for n in myGen(6):
...     print(n)
... 
6
7

De même, il existe expressions de générateur qui permettent de décrire succinctement certains types courants de générateurs :

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Notez que les expressions de générateur ressemblent beaucoup à compréhensions de listes :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Observez qu'un objet générateur est généré une fois mais son code est no s'exécuter en une seule fois. Seuls les appels à next exécuter effectivement (une partie) du code. L'exécution du code dans un générateur s'arrête une fois qu'un code yield a été atteint, après quoi il renvoie une valeur. L'appel suivant à next puis fait en sorte que l'exécution se poursuive dans l'état dans lequel le générateur a été laissé après la dernière yield . Il s'agit d'une différence fondamentale avec les fonctions ordinaires : celles-ci commencent toujours l'exécution au "début" et abandonnent leur état lorsqu'elles renvoient une valeur.

Il y a d'autres choses à dire sur ce sujet. Il est par exemple possible de send dans un générateur ( référence ). Mais je vous suggère de ne pas vous pencher sur cette question avant d'avoir compris le concept de base d'un générateur.

Vous vous demandez peut-être : pourquoi utiliser des générateurs ? Il y a plusieurs bonnes raisons :

  • Certains concepts peuvent être décrits de manière beaucoup plus succincte à l'aide de générateurs.
  • Au lieu de créer une fonction qui renvoie une liste de valeurs, on peut écrire un générateur qui génère les valeurs à la volée. Cela signifie qu'il n'est pas nécessaire de construire une liste, ce qui signifie que le code résultant est plus efficace en termes de mémoire. De cette manière, il est même possible de décrire des flux de données qui seraient tout simplement trop volumineux pour tenir en mémoire.
  • Les générateurs permettent de décrire de manière naturelle infini les flux. Considérons par exemple le Les nombres de Fibonacci :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Ce code utilise itertools.islice pour prendre un nombre fini d'éléments dans un flux infini. Nous vous conseillons de jeter un coup d'œil aux fonctions de l'application itertools car ce sont des outils essentiels pour écrire des générateurs avancés avec une grande facilité.


   † À propos de Python <=2.6 : dans les exemples ci-dessus next est une fonction qui appelle la méthode __next__ sur l'objet donné. Dans Python <=2.6 on utilise une technique légèrement différente, à savoir o.next() au lieu de next(o) . Python 2.7 a next() appelez .next il n'est donc pas nécessaire d'utiliser ce qui suit en 2.7 :

>>> g = (n for n in range(3, 5))
>>> g.next()
3

13 votes

Vous mentionnez qu'il est possible de send à un générateur. Une fois que vous avez fait cela, vous avez une "coroutine". Il est très simple d'implémenter des patterns comme le Consumer/Producer mentionné avec des coroutines car ils n'ont pas besoin de Lock et ne peuvent donc pas se bloquer. Il est difficile de décrire les coroutines sans dénigrer les threads, je dirai donc simplement que les coroutines sont une alternative très élégante au threading.

0 votes

Les générateurs Python sont-ils fondamentalement des machines de Turing en termes de fonctionnement ?

0 votes

54voto

cjrh Points 3960

Un générateur est en fait une fonction qui renvoie (des données) avant d'être terminée, mais qui se met en pause à ce moment-là, et vous pouvez reprendre la fonction à ce moment-là.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

et ainsi de suite. L'avantage (ou l'un des avantages) des générateurs est que, comme ils traitent les données un par un, vous pouvez traiter de grandes quantités de données ; avec les listes, les besoins excessifs en mémoire pourraient devenir un problème. Les générateurs, tout comme les listes, sont itérables et peuvent donc être utilisés de la même manière :

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Notez que les générateurs fournissent une autre façon de traiter l'infini, par exemple

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Le générateur encapsule une boucle infinie, mais ce n'est pas un problème car vous n'obtenez chaque réponse qu'à chaque fois que vous la demandez.

32voto

nikow Points 8887

Tout d'abord, le terme générateur était à l'origine mal défini dans Python, ce qui a entraîné de nombreuses confusions. Vous voulez probablement dire itérateurs y itérables (voir ici ). Ensuite, en Python, il y a aussi fonctions du générateur (qui renvoient un objet générateur), objets de générateur (qui sont des itérateurs) et expressions de générateur (qui sont évalués en un objet générateur).

Selon l'entrée du glossaire pour générateur il semble que la terminologie officielle soit maintenant que générateur est l'abréviation de "fonction de générateur". Dans le passé, la documentation définissait les termes de manière incohérente, mais heureusement, cela a été corrigé.

Il peut toujours être judicieux d'être précis et d'éviter le terme "générateur" sans autre précision.

3 votes

Hmm je pense que vous avez raison, du moins d'après un test de quelques lignes en Python 2.6. Une expression de générateur renvoie un itérateur (alias 'objet générateur'), pas un générateur.

23voto

overthink Points 9471

Les générateurs peuvent être considérés comme un raccourci pour créer un itérateur. Ils se comportent comme un itérateur Java. Exemple :

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

J'espère que cela vous aidera ou correspondra à ce que vous recherchez.

Mise à jour :

Comme le montrent de nombreuses autres réponses, il existe différentes façons de créer un générateur. Vous pouvez utiliser la syntaxe des parenthèses comme dans mon exemple ci-dessus, ou vous pouvez utiliser le rendement. Une autre caractéristique intéressante est que les générateurs peuvent être "infinis" -- des itérateurs qui ne s'arrêtent pas :

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1 votes

Maintenant, Java a Stream qui sont beaucoup plus similaires aux générateurs, sauf que vous ne pouvez apparemment pas obtenir l'élément suivant sans une quantité surprenante de tracas.

15voto

Wernsey Points 3227

Il n'existe pas d'équivalent en Java.

Voici un exemple un peu artificiel :

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

Il y a une boucle dans le générateur qui va de 0 à n, et si la variable de la boucle est un multiple de 3, elle donne la variable.

Pendant chaque itération de la for boucle le générateur est exécuté. Si c'est la première fois que le générateur s'exécute, il commence au début, sinon il continue à partir de la fois précédente où il a donné un résultat.

2 votes

Le dernier paragraphe est très important : l'état de la fonction générateur est "gelé" à chaque fois qu'elle produit quelque chose, et reste exactement dans le même état lorsqu'elle est invoquée la fois suivante.

0 votes

Il n'y a pas d'équivalent syntaxique en Java pour une "expression de générateur", mais les générateurs - une fois que vous en avez un - sont essentiellement des itérateurs (mêmes caractéristiques de base qu'un itérateur Java).

1 votes

@overthink : Eh bien, les générateurs peuvent avoir d'autres effets secondaires que les itérateurs Java ne peuvent pas avoir. Si je devais mettre print "hello" après le x=x+1 dans mon exemple, "hello" serait imprimé 100 fois, alors que le corps de la boucle for ne serait toujours exécuté que 33 fois.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X