web-dev-qa-db-fra.com

Comprendre les générateurs dans Python

Je suis en train de lire le livre de recettes Python) et je suis en train de regarder des générateurs. J'ai du mal à comprendre.

Comme je viens d'un contexte Java, y a-t-il un équivalent Java? Le livre parlait de 'Producteur/Consommateur', cependant de filetage.

Qu'est-ce qu'un générateur et pourquoi l'utiliseriez-vous? Sans citer aucun livre, évidemment (à moins que vous ne trouviez une réponse décente et simpliste directement à partir d’un livre). Peut-être avec des exemples, si vous vous sentez généreux!

192
Federer

Remarque: cette publication suppose que Python 3.x.

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

Les fonctions normales renvoient une valeur unique en utilisant return, comme en Java. En Python, cependant, il existe une alternative, appelée yield. Utiliser 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 constater, myGen(n) est une fonction qui donne n et n + 1. Chaque appel à next donne une valeur unique, jusqu'à ce que toutes les valeurs aient été fournies. for boucle l'appel next en arrière-plan, ainsi:

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

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

>>> 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 du générateur ressemblent beaucoup à liste compréhensions :

>>> 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 pas exécuté en une fois. Seuls les appels à next exécutent réellement (partiellement) le code. L'exécution du code dans un générateur s'arrête dès qu'une instruction yield est atteinte, sur laquelle une valeur est renvoyée. L'appel suivant à next provoque alors la poursuite de l'exécution dans l'état dans lequel le générateur était laissé après le dernier yield. Il s’agit d’une différence fondamentale avec les fonctions standard: celles-ci commencent toujours l’exécution par le haut et abandonnent leur état lors du renvoi d’une valeur.

Il y a plus de choses à dire sur ce sujet. C'est par exemple possible de send données dans un générateur ( référence ). Mais c'est quelque chose que je vous suggère de ne pas examiner avant d'avoir compris le concept de base d'un générateur.

Maintenant, vous pouvez demander: pourquoi utiliser des générateurs? Il y a deux bonnes raisons:

  • Certains concepts peuvent être décrits beaucoup plus succinctement à l'aide de générateurs.
  • Au lieu de créer une fonction qui retourne une liste de valeurs, on peut écrire un générateur qui génère les valeurs à la volée. Cela signifie qu'aucune liste ne doit être construite, ce qui signifie que le code résultant utilise plus efficacement la mémoire. De cette manière, on peut même décrire des flux de données qui seraient simplement trop volumineux pour tenir dans la mémoire.
  • Les générateurs permettent de décrire de manière naturelle les flux infinis. Considérons par exemple le 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 bien regarder les fonctions du module itertools , car ce sont des outils essentiels pour écrire facilement des générateurs avancés.


  À 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() call .next. Vous n'avez donc pas besoin d'utiliser les éléments suivants dans la version 2.7:

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

Un générateur est effectivement une fonction qui retourne (données) avant la fin, mais il 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

etc. L’avantage (ou l’un des) avantages des générateurs est qu’ils peuvent traiter de grandes quantités de données, car ils traitent les données un à un. avec des listes, des besoins excessifs en mémoire pourraient devenir un problème. Les générateurs, tout comme les listes, sont éditables, ils 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 un autre moyen 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 que chaque réponse à chaque fois que vous le demandez.

47
Caleb Hattingh

Tout d'abord, le terme générateur était quelque peu mal défini en Python, ce qui a entraîné beaucoup de confusion. Vous voulez probablement dire itérateurs et itérables (voir ici ). Ensuite, dans Python, il existe aussi des fonctions de générateur (qui renvoient un objet générateur), objets générateurs (qui sont des itérateurs) et expressions génératrices (qui sont évalués en tant qu'objet générateur).

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

Ce pourrait être une bonne idée d’être précis et d’éviter le terme "générateur" sans autre précision.

26
nikow

Les générateurs pourraient être considérés comme un raccourci pour créer un itérateur. Ils se comportent comme un Java Iterator. 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 aide/est ce que vous cherchez.

Mise à jour:

Comme de nombreuses autres réponses le montrent, il existe différentes manières 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
...
22
overthink

Il n'y a pas d'équivalent 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

Une boucle dans le générateur va de 0 à n et si la variable de boucle est un multiple de 3, la variable est générée.

Lors de chaque itération de la boucle for, le générateur est exécuté. Si c'est la première fois que le générateur s'exécute, il commence par le début, sinon il continue à partir du moment précédent.

11
Wernsey

J'aime décrire les générateurs, à ceux qui ont une expérience décente en langages de programmation et en informatique, en termes de cadres de pile.

Dans de nombreuses langues, il y a une pile au dessus de laquelle se trouve la pile actuelle "frame". Le cadre de pile comprend un espace alloué pour les variables locales à la fonction, y compris les arguments transmis à cette fonction.

Lorsque vous appelez une fonction, le point d'exécution actuel (le "compteur de programme" ou son équivalent) est poussé sur la pile et un nouveau cadre de pile est créé. L'exécution est ensuite transférée au début de la fonction appelée.

Avec les fonctions normales, la fonction renvoie à un moment donné une valeur et la pile est "éclatée". Le cadre de pile de la fonction est ignoré et l'exécution reprend à l'emplacement précédent.

Lorsqu'une fonction est un générateur, elle peut renvoyer une valeur sans le frame de pile en cours d'élimination, à l'aide de l'instruction de rendement. Les valeurs des variables locales et le compteur de programme dans la fonction sont conservés. Cela permet au générateur de reprendre plus tard, avec l'exécution continue à partir de l'instruction de rendement, et il peut exécuter plus de code et renvoyer une autre valeur.

Avant Python 2.5, tous les générateurs le faisaient. Python 2.5 ajoutait également la possibilité de transmettre des valeurs in au générateur. Ce faisant, la valeur transmise est disponible sous la forme d'une expression résultant de l'instruction de rendement qui avait temporairement renvoyé le contrôle (et une valeur) du générateur.

L'avantage clé des générateurs est que "l'état" de la fonction est préservé, contrairement aux fonctions classiques dans lesquelles chaque fois que le cadre de pile est ignoré, vous perdez tout cet "état". Un avantage secondaire est que l’on évite une partie de la surcharge d’appel de fonction (création et suppression de trames de pile), bien qu’il s’agisse généralement d’un avantage mineur.

8
Peter Hansen

Il est utile de faire une distinction claire entre la fonction foo et le générateur foo (n):

def foo(n):
    yield n
    yield n+1

foo est une fonction. truc (6) est un objet générateur.

La manière typique d'utiliser un objet générateur est dans une boucle:

for n in foo(6):
    print(n)

La boucle imprime

# 6
# 7

Pensez à un générateur comme une fonction pouvant être reprise.

yield se comporte comme return dans le sens où les valeurs générées sont "retournées" par le générateur. Contrairement à return, toutefois, la prochaine fois que le générateur se voit demander une valeur, la fonction du générateur, foo, reprend là où elle s’était arrêtée - après la dernière déclaration de rendement - et continue de fonctionner jusqu’à ce qu’elle atteigne une autre déclaration de rendement.

En coulisse, lorsque vous appelez bar=foo(6), la barre d'objets du générateur est définie pour que vous ayez un attribut next.

Vous pouvez appeler cela vous-même pour récupérer les valeurs générées par foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Lorsque foo se termine (et qu'il n'y a plus de valeurs renvoyées), appeler next(bar) génère une erreur StopInteration.

6
unutbu

La seule chose que je puisse ajouter à la réponse de Stephan202 est de vous recommander de jeter un coup d'œil à la présentation PyCon '08 de David Beazley intitulée "Astuces de générateur pour les programmeurs de systèmes", ce qui constitue la meilleure explication du comment et du pourquoi des générateurs que j'ai vus. nulle part. C’est ce qui m’a amené de "Python a l’air plutôt amusant" à "C’est ce que je cherchais". C'est à http://www.dabeaz.com/generators/ .

6
Robert Rossney

Ce message utilisera nombres de Fibonacci comme outil permettant d'expliquer l'utilité de générateurs Python .

Ce message présentera à la fois le code C++ et le code Python.

Les nombres de Fibonacci sont définis comme la séquence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Ou en général:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Cela peut être transféré dans une fonction C++ extrêmement facilement:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Mais si vous voulez imprimer les six premiers nombres de Fibonacci, vous recalculerez beaucoup de valeurs avec la fonction ci-dessus.

Par exemple: Fib(3) = Fib(2) + Fib(1), mais Fib(2) recalcule également Fib(1). Plus la valeur que vous souhaitez calculer est élevée, plus vous serez défavorisé.

On peut donc être tenté de réécrire ce qui précède en gardant une trace de l’état dans main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Mais c’est très moche et cela complique notre logique dans main. Il serait préférable de ne pas avoir à s'inquiéter de l'état dans notre fonction main.

Nous pourrions retourner un vector de valeurs et utiliser un iterator pour itérer sur cet ensemble de valeurs, mais cela nécessite beaucoup de mémoire en même temps pour un grand nombre de valeurs de retour.

Donc, revenons à notre ancienne approche, que se passe-t-il si nous voulions faire autre chose que d’imprimer les chiffres? Nous devrions copier et coller tout le bloc de code dans main et modifier les instructions de sortie pour faire ce que nous voudrions faire d'autre. Et si vous copiez et collez du code, vous devriez être abattu. Vous ne voulez pas vous faire tirer dessus, n'est-ce pas?

Pour résoudre ces problèmes et éviter de vous faire tirer dessus, nous pouvons réécrire ce bloc de code en utilisant une fonction de rappel. Chaque fois qu'un nouveau numéro Fibonacci est rencontré, nous appelons la fonction de rappel.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Ceci est clairement une amélioration, votre logique dans main n'est pas aussi encombrée, et vous pouvez faire ce que vous voulez avec les numéros Fibonacci, définissez simplement de nouveaux rappels.

Mais ce n'est toujours pas parfait. Et si vous vouliez obtenir uniquement les deux premiers nombres de Fibonacci, puis faire quelque chose, puis en obtenir davantage, puis faire autre chose?

Eh bien, nous pourrions continuer comme avant et ajouter de nouveau l'état dans main, permettant ainsi à GetFibNumbers de partir d'un point arbitraire. Mais cela va alourdir davantage notre code, et il semble déjà trop gros pour une tâche aussi simple que celle d’imprimer des nombres de Fibonacci.

Nous pourrions mettre en œuvre un modèle de producteur et de consommateur via quelques fils. Mais cela complique encore plus le code.

Parlons plutôt des générateurs.

Python possède une fonctionnalité de langage très agréable qui résout des problèmes tels que ceux appelés générateurs.

Un générateur vous permet d'exécuter une fonction, de vous arrêter à un moment quelconque, puis de continuer là où vous l'avez laissé. Chaque fois renvoyant une valeur.

Considérez le code suivant qui utilise un générateur:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Ce qui nous donne les résultats:

0 1 1 2 3 5

L’instruction yield est utilisée conjointement avec les générateurs Python. Elle enregistre l’état de la fonction et renvoie la valeur yeilded. La prochaine fois que vous appelez la fonction next () le générateur, il continuera là où le rendement s'est arrêté.

Ceci est de loin plus propre que le code de la fonction de rappel. Nous avons un code plus propre, un code plus petit et un code beaucoup plus fonctionnel (Python autorise des entiers arbitrairement grands).

Source

4
Brian R. Bondy

Je pense que la première apparition d'itérateurs et de générateurs s'est faite dans le langage de programmation Icon, il y a environ 20 ans.

Vous pouvez apprécier la vue d'ensemble des icônes , ce qui vous permet de les comprendre sans vous concentrer sur la syntaxe (puisque Icon est une langue que vous ne connaissez probablement pas, et Griswold expliquait les avantages de sa langue. personnes venant d'autres langues).

Après avoir lu quelques paragraphes, l'utilité des générateurs et des itérateurs pourrait devenir plus apparente.

2
Nosredna

L'expérience de la compréhension de liste a montré leur utilité répandue dans tout Python. Cependant, de nombreux cas d'utilisation n'ont pas besoin d'avoir une liste complète créée en mémoire. Au lieu de cela, ils doivent seulement parcourir les éléments un par un.

Par exemple, le code de sommation suivant créera une liste complète de carrés en mémoire, itérera sur ces valeurs et, lorsque la référence ne sera plus nécessaire, supprimera la liste:

sum([x*x for x in range(10)])

La mémoire est conservée en utilisant une expression génératrice à la place:

sum(x*x for x in range(10))

Des avantages similaires sont conférés aux constructeurs pour les objets conteneur:

s = Set(Word  for line in page  for Word in line.split())
d = dict( (k, func(k)) for k in keylist)

Les expressions de générateur sont particulièrement utiles avec des fonctions telles que sum (), min () et max () qui réduisent une entrée itérable à une valeur unique:

max(len(line)  for line in file  if line.strip())

plus

2
Saqib Mujtaba