Que fait le mot-clé «yield»?


10200

À quoi sert le yieldmot - clé en Python, et que fait-il?

Par exemple, j'essaie de comprendre ce code 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

Et voici l'appelant:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Que se passe-t-il lorsque la méthode _get_child_candidatesest appelée? Une liste est-elle retournée? Un seul élément? Est-il appelé à nouveau? Quand les appels suivants s'arrêteront-ils?


1. Ce morceau de code a été écrit par Jochen Schulz (jrschulz), qui a fait une excellente bibliothèque Python pour les espaces métriques. Ceci est le lien vers la source complète: Module mspace .

Réponses:


14656

Pour comprendre ce qui yieldfonctionne, vous devez comprendre ce que sont les générateurs . Et avant de pouvoir comprendre les générateurs, vous devez comprendre les itérables .

Iterables

Lorsque vous créez une liste, vous pouvez lire ses éléments un par un. La lecture de ses éléments un par un est appelée itération:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistest un itérable . Lorsque vous utilisez une compréhension de liste, vous créez une liste, et donc un itérable:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Tout ce que vous pouvez utiliser sur " for... in..." est un itérable; lists, strings, Fichiers ...

Ces itérables sont pratiques car vous pouvez les lire autant que vous le souhaitez, mais vous stockez toutes les valeurs en mémoire et ce n'est pas toujours ce que vous voulez quand vous avez beaucoup de valeurs.

Générateurs

Les générateurs sont des itérateurs, une sorte d'itérable que vous ne pouvez répéter qu'une seule fois . Les générateurs ne stockent pas toutes les valeurs en mémoire, ils génèrent les valeurs à la volée :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

C'est la même chose, sauf que vous avez utilisé à la ()place de []. MAIS, vous ne pouvez pas effectuer for i in mygeneratorune deuxième fois car les générateurs ne peuvent être utilisés qu'une seule fois: ils calculent 0, puis l'oublient et calculent 1, et terminent le calcul 4, un par un.

rendement

yieldest un mot-clé utilisé comme return, sauf que la fonction retournera un générateur.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Ici, c'est un exemple inutile, mais c'est pratique lorsque vous savez que votre fonction renverra un énorme ensemble de valeurs que vous n'aurez besoin de lire qu'une seule fois.

Pour maîtriser yield, vous devez comprendre que lorsque vous appelez la fonction, le code que vous avez écrit dans le corps de la fonction ne s'exécute pas. La fonction ne renvoie que l'objet générateur, c'est un peu délicat :-)

Ensuite, votre code continuera là où il s'était arrêté à chaque forutilisation du générateur.

Maintenant, la partie difficile:

La première fois que l' forappel à l'objet générateur créé à partir de votre fonction, il exécutera le code dans votre fonction depuis le début jusqu'à ce qu'il frappe yield, puis il renverra la première valeur de la boucle. Ensuite, chaque appel suivant exécutera une autre itération de la boucle que vous avez écrite dans la fonction et renverra la valeur suivante. Cela continuera jusqu'à ce que le générateur soit considéré comme vide, ce qui se produit lorsque la fonction s'exécute sans frapper yield. Cela peut être dû au fait que la boucle est terminée ou parce que vous ne remplissez plus un "if/else".


Votre code expliqué

Générateur:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Votre interlocuteur:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Ce code contient plusieurs parties intelligentes:

  • La boucle itère sur une liste, mais la liste se développe pendant que la boucle est itérée :-) C'est un moyen concis de parcourir toutes ces données imbriquées même si c'est un peu dangereux car vous pouvez vous retrouver avec une boucle infinie. Dans ce cas, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))épuisez toutes les valeurs du générateur, mais whilecontinue de créer de nouveaux objets générateurs qui produiront des valeurs différentes des précédentes car elles ne sont pas appliquées sur le même nœud.

  • La extend()méthode est une méthode d'objet de liste qui attend un itérable et ajoute ses valeurs à la liste.

Habituellement, nous lui passons une liste:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Mais dans votre code, il obtient un générateur, ce qui est bien car:

  1. Vous n'avez pas besoin de lire les valeurs deux fois.
  2. Vous pouvez avoir beaucoup d'enfants et vous ne voulez pas qu'ils soient tous stockés en mémoire.

Et cela fonctionne parce que Python ne se soucie pas si l'argument d'une méthode est une liste ou non. Python attend des itérables donc il fonctionnera avec des chaînes, des listes, des tuples et des générateurs! C'est ce qu'on appelle la frappe de canard et c'est l'une des raisons pour lesquelles Python est si cool. Mais ceci est une autre histoire, pour une autre question ...

Vous pouvez vous arrêter ici ou lire un peu pour voir une utilisation avancée d'un générateur:

Contrôler l'épuisement d'un générateur

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Remarque: Pour Python 3, utilisez print(corner_street_atm.__next__())ouprint(next(corner_street_atm))

Il peut être utile pour diverses choses comme le contrôle de l'accès à une ressource.

Itertools, votre meilleur ami

Le module itertools contient des fonctions spéciales pour manipuler les itérables. Vous avez toujours voulu dupliquer un générateur? Chaîne de deux générateurs? Regrouper les valeurs dans une liste imbriquée avec une ligne? Map / Zipsans créer une autre liste?

Alors juste import itertools.

Un exemple? Voyons les éventuels ordres d'arrivée pour une course à quatre chevaux:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Comprendre les mécanismes internes de l'itération

L'itération est un processus impliquant des itérables (implémentation de la __iter__()méthode) et des itérateurs (implémentation de la __next__()méthode). Les itérables sont tous les objets dont vous pouvez obtenir un itérateur. Les itérateurs sont des objets qui vous permettent d'itérer sur les itérables.

Il y a plus à ce sujet dans cet article sur le fonctionnement des forboucles .


355
yieldn'est pas aussi magique que cette réponse le suggère. Lorsque vous appelez une fonction qui contient une yieldinstruction n'importe où, vous obtenez un objet générateur, mais aucun code ne s'exécute. Ensuite, chaque fois que vous extrayez un objet du générateur, Python exécute du code dans la fonction jusqu'à ce qu'il arrive à une yieldinstruction, puis met en pause et délivre l'objet. Lorsque vous extrayez un autre objet, Python reprend juste après le yieldet continue jusqu'à ce qu'il atteigne un autre yield(souvent le même, mais une itération plus tard). Cela continue jusqu'à la fin de la fonction, moment auquel le générateur est réputé épuisé.
Matthias Fripp

30
"Ces itérables sont pratiques ... mais vous stockez toutes les valeurs en mémoire et ce n'est pas toujours ce que vous voulez", est soit faux soit déroutant. Un itérable renvoie un itérateur en appelant l'iter () sur l'itérable, et un itérateur n'a pas toujours à stocker ses valeurs en mémoire, selon la mise en œuvre de la méthode iter , il peut également générer des valeurs dans la séquence à la demande.
picmate 涅

Ce serait bien d'ajouter à cette excellente réponse pourquoi c'est la même chose, sauf que vous avez utilisé à la ()place de[] , en particulier ce qui ()est (il peut y avoir de la confusion avec un tuple).
WoJ

Je me trompe peut-être, mais un générateur n'est pas un itérateur, un "générateur appelé" est un itérateur.
aderchox

@MatthiasFripp "Cela continue jusqu'à la fin de la fonction" - ou il rencontre une returninstruction. ( returnest autorisé dans une fonction contenant yield, à condition de ne pas spécifier de valeur de retour.)
alaniwi

2007

Raccourci vers la compréhension yield

Lorsque vous voyez une fonction avec des yieldinstructions, appliquez cette astuce simple pour comprendre ce qui se passera:

  1. Insérez une ligne result = []au début de la fonction.
  2. Remplacez chacun yield exprpar result.append(expr).
  3. Insérez une ligne return resultau bas de la fonction.
  4. Ouais - plus de yielddéclarations! Lisez et comprenez le code.
  5. Comparer la fonction à la définition d'origine.

Cette astuce peut vous donner une idée de la logique derrière la fonction, mais ce qui se passe réellement avec yieldest considérablement différent de ce qui se passe dans l'approche basée sur une liste. Dans de nombreux cas, l'approche du rendement sera beaucoup plus efficace en mémoire et plus rapide. Dans d'autres cas, cette astuce vous coincera dans une boucle infinie, même si la fonction d'origine fonctionne très bien. Continuez à lire pour en savoir plus...

Ne confondez pas vos Iterables, itérateurs et générateurs

Tout d'abord, le protocole de l' itérateur - lorsque vous écrivez

for x in mylist:
    ...loop body...

Python effectue les deux étapes suivantes:

  1. Obtient un itérateur pour mylist:

    Appel iter(mylist)-> cela renvoie un objet avec une next()méthode (ou __next__()en Python 3).

    [C'est l'étape que la plupart des gens oublient de vous parler]

  2. Utilise l'itérateur pour parcourir les éléments:

    Continuez d'appeler la next()méthode sur l'itérateur renvoyé par l'étape 1. La valeur de retour de next()est affectée à xet le corps de la boucle est exécuté. Si une exception StopIterationest levée de l'intérieur next(), cela signifie qu'il n'y a plus de valeurs dans l'itérateur et que la boucle est fermée.

La vérité est que Python exécute les deux étapes ci-dessus chaque fois qu'il veut faire une boucle sur le contenu d'un objet - il pourrait donc s'agir d'une boucle for, mais cela pourrait également être du code otherlist.extend(mylist)(où otherlistest une liste Python).

Voici mylistun itérable car il implémente le protocole itérateur. Dans une classe définie par l'utilisateur, vous pouvez implémenter la __iter__()méthode pour rendre les instances de votre classe itérables. Cette méthode doit renvoyer un itérateur . Un itérateur est un objet avec une next()méthode. Il est possible d'implémenter les deux __iter__()et next()sur la même classe, et d'avoir un __iter__()retour self. Cela fonctionnera pour les cas simples, mais pas lorsque vous souhaitez que deux itérateurs bouclent sur le même objet en même temps.

Voilà donc le protocole itérateur, de nombreux objets implémentent ce protocole:

  1. Listes intégrées, dictionnaires, tuples, ensembles, fichiers.
  2. Classes définies par l'utilisateur qui implémentent __iter__().
  3. Générateurs.

Notez qu'une forboucle ne sait pas de quel type d'objet il s'agit - elle suit simplement le protocole de l'itérateur, et est heureuse d'obtenir élément après élément lors de son appel next(). Les listes intégrées renvoient leurs éléments un par un, les dictionnaires renvoient les clés une par une, les fichiers renvoient les lignes une par une, etc. Et les générateurs reviennent ... eh bien, c'est là yieldqu'intervient:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Au lieu d' yieldinstructions, si vous aviez trois returninstructions, f123()seule la première serait exécutée et la fonction se fermerait. Mais ce f123()n'est pas une fonction ordinaire. Quand f123()est appelé, il ne renvoie aucune des valeurs dans les déclarations de rendement! Il renvoie un objet générateur. De plus, la fonction ne sort pas vraiment - elle entre dans un état suspendu. Lorsque la forboucle essaie de faire une boucle sur l'objet générateur, la fonction reprend à partir de son état suspendu à la ligne suivante après le yieldretour précédent, exécute la ligne de code suivante, dans ce cas, une yieldinstruction, et la renvoie en tant que prochaine article. Cela se produit jusqu'à ce que la fonction se termine, à quel point le générateur monte StopIteration, et la boucle se termine.

Ainsi, l'objet générateur est un peu comme un adaptateur - à une extrémité, il présente le protocole itérateur, en exposant __iter__()et en next()méthodes pour garder la forboucle heureuse. À l'autre extrémité, cependant, il exécute la fonction juste assez pour en extraire la valeur suivante et la remet en mode suspendu.

Pourquoi utiliser des générateurs?

Habituellement, vous pouvez écrire du code qui n'utilise pas de générateurs mais implémente la même logique. Une option consiste à utiliser la «liste» de liste temporaire que j'ai mentionnée précédemment. Cela ne fonctionnera pas dans tous les cas, par exemple si vous avez des boucles infinies, ou cela peut faire un usage inefficace de la mémoire lorsque vous avez une liste vraiment longue. L'autre approche consiste à implémenter une nouvelle classe itérable SomethingIter qui conserve l'état dans les membres d'instance et effectue la prochaine étape logique dans sa méthode next()(ou __next__()en Python 3). Selon la logique, le code à l'intérieur de la next()méthode peut sembler très complexe et être sujet à des bogues. Ici, les générateurs offrent une solution propre et facile.


20
"Lorsque vous voyez une fonction avec des déclarations de rendement, appliquez cette astuce simple pour comprendre ce qui va se passer". Cela n'ignore-t-il pas complètement le fait que vous pouvez sendintégrer un générateur, ce qui représente une grande partie de l'intérêt des générateurs?
DanielSank

10
"cela pourrait être une boucle for, mais cela pourrait aussi être du code comme otherlist.extend(mylist)" -> C'est incorrect. extend()modifie la liste sur place et ne renvoie pas d'itérable. Essayer de boucler otherlist.extend(mylist)échouera avec un TypeErrorcar il extend()revient implicitement None, et vous ne pouvez pas boucler None.
Pedro

4
@pedro Vous avez mal compris cette phrase. Cela signifie que python exécute les deux étapes mentionnées lors de l'exécution mylist(et non otherlistlors de l'exécution) otherlist.extend(mylist).
aujourd'hui

555

Pense-y de cette façon:

Un itérateur est juste un terme de fantaisie pour un objet qui a une next()méthode. Donc, une fonction cédée finit par ressembler à ceci:

Version originale:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

C'est essentiellement ce que fait l'interpréteur Python avec le code ci-dessus:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Pour plus d'informations sur ce qui se passe dans les coulisses, la forboucle peut être réécrite comme suit:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Est-ce que cela a plus de sens ou vous embrouille plus? :)

Je dois noter qu'il s'agit d' une simplification à des fins d'illustration. :)


1
__getitem__pourrait être défini au lieu de __iter__. Par exemple class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)
:,

17
J'ai essayé cet exemple en Python 3.6 et si je crée iterator = some_function(), la variable iteratorn'a plus de fonction appelée next(), mais seulement une __next__()fonction. Je pensais que je le mentionnerais.
Peter

Où l' forimplémentation de boucle que vous avez écrite appelle la __iter__méthode iterator, l'instance instanciée de it?
Désintégration systématique

455

Le yieldmot-clé est réduit à deux faits simples:

  1. Si le compilateur détecte le yieldmot clé n'importe où dans une fonction, cette fonction ne retourne plus via l' returninstruction. Au lieu de cela , il renvoie immédiatement un objet "liste en attente" paresseux appelé générateur
  2. Un générateur est itérable. Qu'est-ce qu'un itérable ? C'est quelque chose comme une vue listou setou rangeou dict, avec un protocole intégré pour visiter chaque élément dans un certain ordre .

En bref: un générateur est une liste paresseuse en attente incrémentielle , et les yieldinstructions vous permettent d'utiliser la notation de fonction pour programmer les valeurs de liste que le générateur doit cracher de manière incrémentielle.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Exemple

Définissons une fonction makeRangequi ressemble à celle de Python range. Appeler makeRange(n)RETOURNE UN GÉNÉRATEUR:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Pour forcer le générateur à renvoyer immédiatement ses valeurs en attente, vous pouvez le passer dans list()(comme vous pouvez le faire):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Exemple de comparaison avec "renvoyer simplement une liste"

L'exemple ci-dessus peut être considéré comme créant simplement une liste que vous ajoutez et renvoyez:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Il existe cependant une différence majeure; voir la dernière section.


Comment vous pourriez utiliser des générateurs

Un itérable est la dernière partie d'une compréhension de liste, et tous les générateurs sont itérables, ils sont donc souvent utilisés comme ceci:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Pour avoir une meilleure idée des générateurs, vous pouvez jouer avec le itertoolsmodule (assurez-vous d'utiliser chain.from_iterableplutôt que chainlorsque cela est justifié). Par exemple, vous pouvez même utiliser des générateurs pour implémenter des listes paresseuses infiniment longues comme itertools.count(). Vous pouvez implémenter le vôtre def enumerate(iterable): zip(count(), iterable)ou le faire avec le yieldmot clé dans une boucle while.

Veuillez noter: les générateurs peuvent en fait être utilisés pour bien d'autres choses, telles que la mise en œuvre de coroutines ou de programmation non déterministe ou d'autres choses élégantes. Cependant, le point de vue "listes paresseuses" que je présente ici est l'utilisation la plus courante que vous trouverez.


Dans les coulisses

C'est ainsi que fonctionne le "protocole d'itération Python". C'est-à-dire ce qui se passe quand vous le faites list(makeRange(5)). C'est ce que je décris plus tôt comme une "liste incrémentielle paresseuse".

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

La fonction intégrée next()appelle simplement la .next()fonction objets , qui fait partie du "protocole d'itération" et se trouve sur tous les itérateurs. Vous pouvez utiliser manuellement la next()fonction (et d'autres parties du protocole d'itération) pour implémenter des choses fantaisistes, généralement au détriment de la lisibilité, alors essayez d'éviter de le faire ...


Menus détails

Normalement, la plupart des gens ne se soucieraient pas des distinctions suivantes et voudraient probablement arrêter de lire ici.

En langage Python, un itérable est tout objet qui "comprend le concept d'une boucle for" comme une liste [1,2,3], et un itérateur est une instance spécifique de la boucle for demandée [1,2,3].__iter__(). Un générateur est exactement le même que n'importe quel itérateur, à l'exception de la façon dont il a été écrit (avec la syntaxe de fonction).

Lorsque vous demandez un itérateur à partir d'une liste, il crée un nouvel itérateur. Cependant, lorsque vous demandez un itérateur à un itérateur (ce que vous feriez rarement), il vous donne simplement une copie de lui-même.

Ainsi, dans le cas peu probable où vous omettez de faire quelque chose comme ça ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... alors souvenez-vous qu'un générateur est un itérateur ; c'est-à-dire qu'il s'agit d'une utilisation unique. Si vous souhaitez le réutiliser, vous devez rappeler myRange(...). Si vous devez utiliser le résultat deux fois, convertissez le résultat en liste et stockez-le dans une variable x = list(myRange(5)). Ceux qui ont absolument besoin de cloner un générateur (par exemple, qui effectuent une métaprogrammation terrifiante et piratée) peuvent utiliser itertools.teesi cela est absolument nécessaire, car la proposition de normes Python PEP de l' itérateur copiable a été différée.


378

Que fait le yieldmot-clé en Python?

Aperçu / Résumé de la réponse

  • Une fonction avec yield, lorsqu'elle est appelée, renvoie un générateur .
  • Les générateurs sont des itérateurs car ils implémentent le protocole d'itérateur , vous pouvez donc les parcourir.
  • Un générateur peut également recevoir des informations , ce qui en fait conceptuellement une coroutine .
  • Dans Python 3, vous pouvez déléguer d'un générateur à un autre dans les deux sens avec yield from.
  • (L'annexe critique quelques réponses, y compris la première, et discute de l'utilisation de returndans un générateur.)

Générateurs:

yieldn'est légal qu'à l'intérieur d'une définition de fonction, et l'inclusion de yielddans une définition de fonction fait qu'il retourne un générateur.

L'idée de générateurs vient d'autres langages (voir référence 1) avec différentes implémentations. Dans les générateurs de Python, l'exécution du code est gelée au point du rendement. Lorsque le générateur est appelé (les méthodes sont décrites ci-dessous), l'exécution reprend, puis se bloque au rendement suivant.

yieldfournit un moyen facile d' implémenter le protocole itérateur , défini par les deux méthodes suivantes: __iter__et next(Python 2) ou __next__(Python 3). Ces deux méthodes font d'un objet un itérateur que vous pouvez vérifier avec la Iteratorclasse de base abstraite du collectionsmodule.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Le type de générateur est un sous-type d'itérateur:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Et si nécessaire, nous pouvons effectuer une vérification de type comme ceci:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Une caractéristique d'un Iterator est qu'une fois épuisé , vous ne pouvez pas le réutiliser ou le réinitialiser:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Vous devrez en créer un autre si vous souhaitez utiliser à nouveau ses fonctionnalités (voir référence 2):

>>> list(func())
['I am', 'a generator!']

On peut produire des données par programme, par exemple:

def func(an_iterable):
    for item in an_iterable:
        yield item

Le générateur simple ci-dessus est également équivalent à celui ci-dessous - à partir de Python 3.3 (et non disponible dans Python 2), vous pouvez utiliser yield from:

def func(an_iterable):
    yield from an_iterable

Cependant, yield frompermet également la délégation aux sous-générateurs, ce qui sera expliqué dans la section suivante sur la délégation coopérative avec les sous-coroutines.

Coroutines:

yield forme une expression qui permet d'envoyer des données dans le générateur (voir référence 3)

Voici un exemple, notez la receivedvariable, qui pointera vers les données envoyées au générateur:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Tout d' abord, il faut faire la queue du générateur avec la fonction builtin, next. Il appellera la méthode appropriée nextou __next__, selon la version de Python que vous utilisez:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

Et maintenant, nous pouvons envoyer des données dans le générateur. (L' envoi Noneest identique à l'appelnext .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Délégation coopérative à Sub-Coroutine avec yield from

Maintenant, rappelez-vous que yield fromc'est disponible dans Python 3. Cela nous permet de déléguer des coroutines à un sous-programme:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

Et maintenant, nous pouvons déléguer des fonctionnalités à un sous-générateur et il peut être utilisé par un générateur comme ci-dessus:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Vous pouvez en savoir plus sur la sémantique précise du yield fromau PEP 380.

Autres méthodes: fermer et lancer

La closeméthode déclenche GeneratorExitau moment où l'exécution de la fonction a été gelée. Cela sera également appelé par __del__afin que vous puissiez mettre n'importe quel code de nettoyage où vous gérez GeneratorExit:

>>> my_account.close()

Vous pouvez également lever une exception qui peut être gérée dans le générateur ou renvoyée à l'utilisateur:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Conclusion

Je pense avoir couvert tous les aspects de la question suivante:

Que fait le yieldmot-clé en Python?

Il s'avère que cela yieldfait beaucoup. Je suis sûr que je pourrais ajouter des exemples encore plus approfondis à cela. Si vous en voulez plus ou si vous avez des critiques constructives, faites-le moi savoir en commentant ci-dessous.


Annexe:

Critique de la réponse du haut / acceptée **

  • Il est confus sur ce qui rend un itérable , en utilisant simplement une liste comme exemple. Voir mes références ci-dessus, mais en résumé: un itérable a une __iter__méthode renvoyant un itérateur . Un itérateur fournit une méthode .next(Python 2 ou .__next__(Python 3), qui est implicitement appelée par des forboucles jusqu'à ce qu'elle augmente StopIteration, et une fois qu'elle le fait, elle continuera de le faire.
  • Il utilise ensuite une expression de générateur pour décrire ce qu'est un générateur. Puisqu'un générateur est simplement un moyen pratique de créer un itérateur , il ne fait que confondre la question, et nous n'avons pas encore atteint la yieldpartie.
  • Dans Le contrôle d' un épuisement du générateur , il appelle la .nextméthode, quand au contraire , il doit utiliser la fonction builtin, next. Ce serait une couche d'indirection appropriée, car son code ne fonctionne pas en Python 3.
  • Itertools? Ce n'était pas pertinent du yieldtout.
  • Pas de discussion sur les méthodes yieldfournies avec les nouvelles fonctionnalités yield fromde Python 3. La réponse supérieure / acceptée est une réponse très incomplète.

Critique de la réponse suggérant yielddans un générateur l'expression ou la compréhension.

La grammaire permet actuellement toute expression dans une liste de compréhension.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Étant donné que le rendement est une expression, certains ont fait valoir qu'il était intéressant de l'utiliser dans des compréhensions ou des expressions génératrices - en dépit de ne citer aucun cas d'utilisation particulièrement bon.

Les développeurs principaux de CPython discutent de la dépréciation de son allocation . Voici un article pertinent de la liste de diffusion:

Le 30 janvier 2017 à 19 h 05, Brett Cannon a écrit:

Le dimanche 29 janvier 2017 à 16 h 39, Craig Rodrigues a écrit:

Je suis d'accord avec l'une ou l'autre approche. Laisser les choses telles qu'elles sont en Python 3 n'est pas bon, à mon humble avis.

Mon vote est que ce soit une SyntaxError puisque vous n'obtenez pas ce que vous attendez de la syntaxe.

Je conviens que c'est un endroit sensé pour nous de finir, car tout code reposant sur le comportement actuel est vraiment trop intelligent pour être maintenable.

Pour y arriver, nous voudrons probablement:

  • SyntaxWarning ou DeprecationWarning dans 3.7
  • Avertissement Py3k dans 2.7.x
  • SyntaxError dans 3.8

Vive, Nick.

- Nick Coghlan | ncoghlan sur gmail.com | Brisbane, Australie

De plus, il y a un problème en suspens (10544) qui semble pointer dans le sens que ce n'est jamais une bonne idée (PyPy, une implémentation Python écrite en Python, déclenche déjà des avertissements de syntaxe.)

En résumé, jusqu'à ce que les développeurs de CPython nous disent le contraire: ne mettez pas d' yieldexpression ou de compréhension de générateur.

L' returninstruction dans un générateur

En Python 2 :

Dans une fonction de générateur, l' returninstruction n'est pas autorisée à inclure un expression_list. Dans ce contexte, une mise à nu returnindique que le générateur est terminé et fera StopIterationmonter.

An expression_listest essentiellement un nombre d'expressions séparées par des virgules - essentiellement, en Python 2, vous pouvez arrêter le générateur avec return, mais vous ne pouvez pas retourner de valeur.

En Python 3 :

Dans une fonction de générateur, l' returninstruction indique que le générateur est terminé et provoquera StopIterationune augmentation. La valeur retournée (le cas échéant) est utilisée comme argument pour construire StopIterationet devient l' StopIteration.valueattribut.

Notes de bas de page

  1. Les langages CLU, Sather et Icon ont été référencés dans la proposition visant à introduire le concept de générateurs dans Python. L'idée générale est qu'une fonction peut maintenir l'état interne et générer des points de données intermédiaires à la demande de l'utilisateur. Cela promettait d'être supérieur en performances à d'autres approches, y compris le threading Python , qui n'est même pas disponible sur certains systèmes.

  2. Cela signifie, par exemple, que les xrangeobjets ( rangeen Python 3) ne sont pas des Iterators, même s'ils sont itérables, car ils peuvent être réutilisés. Comme les listes, leurs __iter__méthodes renvoient des objets itérateur.

  3. yielda été initialement présenté comme une instruction, ce qui signifie qu'il ne pouvait apparaître qu'au début d'une ligne dans un bloc de code. Crée maintenant yieldune expression de rendement. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Cette modification a été proposée pour permettre à un utilisateur d'envoyer des données dans le générateur comme on pourrait les recevoir. Pour envoyer des données, il faut être en mesure de les affecter à quelque chose, et pour cela, une déclaration ne fonctionnera tout simplement pas.


328

yieldest comme return- il renvoie tout ce que vous lui dites (en tant que générateur). La différence est que la prochaine fois que vous appelez le générateur, l'exécution commence à partir du dernier appel à l' yieldinstruction. Contrairement au retour, le cadre de pile n'est pas nettoyé lorsqu'un rendement se produit, mais le contrôle est retransféré à l'appelant, donc son état reprendra la prochaine fois que la fonction sera appelée.

Dans le cas de votre code, la fonction get_child_candidatesagit comme un itérateur de sorte que lorsque vous étendez votre liste, elle ajoute un élément à la fois à la nouvelle liste.

list.extendappelle un itérateur jusqu'à épuisement. Dans le cas de l'exemple de code que vous avez publié, il serait beaucoup plus clair de simplement retourner un tuple et de l'ajouter à la liste.


107
C'est proche, mais pas correct. Chaque fois que vous appelez une fonction contenant une déclaration de rendement, elle renvoie un nouvel objet générateur. Ce n'est que lorsque vous appelez la méthode .next () de ce générateur que l'exécution reprend après le dernier rendement.
kurosch

239

Il y a une chose supplémentaire à mentionner: une fonction qui donne n'a pas à se terminer. J'ai écrit du code comme ceci:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Ensuite, je peux l'utiliser dans un autre code comme celui-ci:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Cela aide vraiment à simplifier certains problèmes et facilite le travail avec certaines choses.


233

Pour ceux qui préfèrent un exemple de travail minimal, méditez sur cette session Python interactive:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print(i)
... 
1
2
3
>>> for i in g:
...   print(i)
... 
>>> # Note that this time nothing was printed

209

TL; DR

Au lieu de cela:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

faites ceci:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Chaque fois que vous vous retrouvez à construire une liste à partir de zéro, yieldchaque pièce à la place.

C'était mon premier moment "aha" avec un rendement.


yieldest une façon sucrée de dire

construire une série de trucs

Même comportement:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Comportement différent:

Le rendement est en un seul passage : vous ne pouvez effectuer une itération qu'une seule fois. Lorsqu'une fonction a un rendement, nous l'appelons une fonction de générateur . Et un itérateur est ce qu'il retourne. Ces termes sont révélateurs. Nous perdons la commodité d'un conteneur, mais gagnons la puissance d'une série calculée selon les besoins et arbitrairement longue.

Le rendement est paresseux , il retarde le calcul. Une fonction avec un rendement ne s'exécute pas du tout lorsque vous l'appelez. Il renvoie un objet itérateur qui se souvient de l'endroit où il s'était arrêté. Chaque fois que vous appelez next()l'itérateur (cela se produit dans une boucle for), l'exécution se fait quelques centimètres vers le rendement suivant. returnlève StopIteration et termine la série (c'est la fin naturelle d'une boucle for).

Le rendement est polyvalent . Les données ne doivent pas être stockées toutes ensemble, elles peuvent être mises à disposition une par une. Cela peut être infini.

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Si vous avez besoin de plusieurs passes et que la série n'est pas trop longue, appelez-la simplement list():

>>> list(square_yield(4))
[0, 1, 4, 9]

Choix brillant du mot yieldcar les deux sens s'appliquent:

rendement - produire ou fournir (comme dans l'agriculture)

... fournir les prochaines données de la série.

céder - céder ou abandonner (comme au pouvoir politique)

... abandonner l'exécution du processeur jusqu'à ce que l'itérateur avance.


194

Le rendement vous donne un générateur.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Comme vous pouvez le voir, dans le premier cas, fooconserve la liste entière en mémoire à la fois. Ce n'est pas grave pour une liste à 5 éléments, mais que faire si vous voulez une liste de 5 millions? Non seulement c'est un énorme mangeur de mémoire, mais cela coûte aussi beaucoup de temps à construire au moment où la fonction est appelée.

Dans le deuxième cas, barvous donne juste un générateur. Un générateur est un itérable - ce qui signifie que vous pouvez l'utiliser dans une forboucle, etc., mais chaque valeur n'est accessible qu'une seule fois. Toutes les valeurs ne sont pas non plus stockées en mémoire en même temps; l'objet générateur "se souvient" de l'endroit où il se trouvait dans la boucle la dernière fois que vous l'avez appelé - de cette façon, si vous utilisez un itérable pour (disons) compter jusqu'à 50 milliards, vous n'avez pas à compter jusqu'à 50 milliards tous à la fois et stocker les 50 milliards de chiffres à compter.

Encore une fois, c'est un exemple assez artificiel, vous utiliseriez probablement itertools si vous vouliez vraiment compter jusqu'à 50 milliards. :)

Il s'agit du cas d'utilisation le plus simple des générateurs. Comme vous l'avez dit, il peut être utilisé pour écrire des permutations efficaces, en utilisant yield pour pousser les choses à travers la pile d'appels au lieu d'utiliser une sorte de variable de pile. Les générateurs peuvent également être utilisés pour une traversée d'arbre spécialisée et toutes sortes d'autres choses.


Juste une note - en Python 3, rangeretourne également un générateur au lieu d'une liste, donc vous verriez également une idée similaire, sauf que __repr__/ __str__est remplacé pour afficher un résultat plus agréable, dans ce cas range(1, 10, 2).
It'sNotALie.

189

C'est retourner un générateur. Je ne suis pas particulièrement familier avec Python, mais je pense que c'est le même genre de chose que les blocs d'itérateur de C # si vous les connaissez.

L'idée clé est que le compilateur / interprète / quoi que ce soit fait quelque truc pour que, en ce qui concerne l'appelant, ils puissent continuer à appeler next () et il continuera à renvoyer des valeurs - comme si la méthode du générateur était en pause . Maintenant, évidemment, vous ne pouvez pas vraiment "suspendre" une méthode, donc le compilateur construit une machine d'état pour que vous vous souveniez où vous êtes actuellement et à quoi ressemblent les variables locales, etc. C'est beaucoup plus facile que d'écrire un itérateur vous-même.


167

Il n'y a qu'un seul type de réponse que je ne pense pas avoir encore donné, parmi les nombreuses bonnes réponses qui décrivent comment utiliser les générateurs. Voici la réponse de la théorie du langage de programmation:

L' yieldinstruction en Python renvoie un générateur. Un générateur en Python est une fonction qui renvoie des continuations (et spécifiquement un type de coroutine, mais les continuations représentent le mécanisme le plus général pour comprendre ce qui se passe).

Les continuations dans la théorie des langages de programmation sont un type de calcul beaucoup plus fondamental, mais elles ne sont pas souvent utilisées, car elles sont extrêmement difficiles à raisonner et également très difficiles à implémenter. Mais l'idée de ce qu'est une continuation est simple: c'est l'état d'un calcul qui n'est pas encore terminé. Dans cet état, les valeurs actuelles des variables, les opérations qui n'ont pas encore été effectuées, etc., sont enregistrées. Ensuite, à un certain moment plus tard dans le programme, la suite peut être invoquée, de sorte que les variables du programme sont réinitialisées à cet état et les opérations qui ont été enregistrées sont effectuées.

Les suites, sous cette forme plus générale, peuvent être mises en œuvre de deux manières. De la même call/ccmanière, la pile du programme est littéralement sauvegardée puis lorsque la suite est invoquée, la pile est restaurée.

Dans le style de passage de continuation (CPS), les continuations ne sont que des fonctions normales (uniquement dans les langages où les fonctions sont de première classe) que le programmeur gère explicitement et transmet aux sous-programmes. Dans ce style, l'état du programme est représenté par des fermetures (et les variables qui y sont codées) plutôt que par des variables qui résident quelque part sur la pile. Les fonctions qui gèrent le flux de contrôle acceptent la continuation comme arguments (dans certaines variantes de CPS, les fonctions peuvent accepter plusieurs continuations) et manipulent le flux de contrôle en les appelant simplement en les appelant et en revenant par la suite. Un exemple très simple de style de passage de continuation est le suivant:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

Dans cet exemple (très simpliste), le programmeur enregistre l'opération d'écriture effective du fichier dans une continuation (qui peut potentiellement être une opération très complexe avec de nombreux détails à écrire), puis passe cette continuation (c.-à-d. fermeture de classe) à un autre opérateur qui effectue un traitement supplémentaire, puis l'appelle si nécessaire. (J'utilise beaucoup ce modèle de conception dans la programmation GUI réelle, soit parce qu'il me permet d'économiser des lignes de code ou, plus important encore, pour gérer le flux de contrôle après le déclenchement des événements GUI.)

Le reste de cet article va, sans perte de généralité, conceptualiser les suites comme CPS, car il est beaucoup plus facile à comprendre et à lire.


Parlons maintenant des générateurs en Python. Les générateurs sont un sous-type spécifique de continuation. Alors que les continuations sont généralement capables de sauvegarder l'état d'un calcul (c'est-à-dire la pile d'appels du programme), les générateurs ne peuvent sauvegarder l'état de l'itération que sur un itérateur . Bien que cette définition soit légèrement trompeuse pour certains cas d'utilisation de générateurs. Par exemple:

def f():
  while True:
    yield 4

Il s'agit clairement d'un itérable raisonnable dont le comportement est bien défini - chaque fois que le générateur l'itère, il renvoie 4 (et le fait pour toujours). Mais ce n'est probablement pas le type prototypique d'itérable qui vient à l'esprit lorsque l'on pense aux itérateurs (c.-à for x in collection: do_something(x)-d.). Cet exemple illustre la puissance des générateurs: si quelque chose est un itérateur, un générateur peut sauvegarder l'état de son itération.

Pour réitérer: Les continuations peuvent enregistrer l'état de la pile d'un programme et les générateurs peuvent enregistrer l'état d'itération. Cela signifie que les continuations sont beaucoup plus puissantes que les générateurs, mais aussi que les générateurs sont beaucoup, beaucoup plus faciles. Ils sont plus faciles à implémenter pour le concepteur de langage et plus faciles à utiliser pour le programmeur (si vous avez du temps à graver, essayez de lire et de comprendre cette page sur les suites et appelez / cc ).

Mais vous pouvez facilement implémenter (et conceptualiser) des générateurs comme un cas simple et spécifique de style de passage de continuation:

Chaque fois qu'il yieldest appelé, il indique à la fonction de renvoyer une continuation. Lorsque la fonction est appelée à nouveau, elle démarre là où elle s'est arrêtée. Ainsi, en pseudo-pseudocode (c'est-à-dire pas en pseudocode, mais pas en code), la nextméthode du générateur est essentiellement la suivante:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

où le yieldmot-clé est en fait du sucre syntaxique pour la fonction de générateur réel, essentiellement quelque chose comme:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

N'oubliez pas que ce n'est qu'un pseudocode et que l'implémentation réelle des générateurs en Python est plus complexe. Mais comme exercice pour comprendre ce qui se passe, essayez d'utiliser le style de passage de continuation pour implémenter des objets générateurs sans utiliser le yieldmot - clé.


152

Voici un exemple en langage simple. Je fournirai une correspondance entre les concepts humains de haut niveau et les concepts Python de bas niveau.

Je veux opérer sur une séquence de nombres, mais je ne veux pas m'embêter avec la création de cette séquence, je veux seulement me concentrer sur l'opération que je veux faire. Donc, je fais ce qui suit:

  • Je vous appelle et vous dis que je veux une séquence de nombres qui est produite d'une manière spécifique, et je vous fais savoir quel est l'algorithme.
    Cette étape correspond à l' defentrée de la fonction générateur, c'est-à-dire la fonction contenant a yield.
  • Un peu plus tard, je vous dis: "OK, préparez-vous à me dire la séquence des nombres".
    Cette étape correspond à l'appel de la fonction générateur qui renvoie un objet générateur. Notez que vous ne me donnez pas encore de chiffres; vous prenez juste votre papier et votre crayon.
  • Je vous demande, "dites-moi le numéro suivant", et vous me dites le premier numéro; après cela, vous attendez que je vous demande le prochain numéro. C'est votre travail de vous rappeler où vous étiez, quels chiffres vous avez déjà dit et quel est le prochain numéro. Je me fiche des détails.
    Cette étape correspond à l'appel .next()à l'objet générateur.
  • … Répéter l'étape précédente, jusqu'à…
  • finalement, vous pourriez arriver à une fin. Vous ne me dites pas de chiffre; vous criez, "tenez vos chevaux! J'ai fini! Plus de chiffres!"
    Cette étape correspond à l'objet générateur terminant son travail et levant une StopIterationexception La fonction générateur n'a pas besoin de lever l'exception. Il est déclenché automatiquement lorsque la fonction se termine ou émet a return.

C'est ce que fait un générateur (une fonction qui contient a yield); il commence à s'exécuter, s'arrête chaque fois qu'il fait un yield, et lorsqu'on lui demande un.next() valeur, il continue à partir du dernier point. Il s'intègre parfaitement par conception avec le protocole itérateur de Python, qui décrit comment demander séquentiellement des valeurs.

L'utilisateur le plus connu du protocole itérateur est la forcommande en Python. Donc, chaque fois que vous faites:

for item in sequence:

peu importe qu'il s'agisse d' sequenceune liste, d'une chaîne, d'un dictionnaire ou d'un objet générateur comme décrit ci-dessus; le résultat est le même: vous lisez les éléments une à une.

Notez que la defsaisie d'une fonction qui contient un yieldmot-clé n'est pas le seul moyen de créer un générateur; c'est juste le moyen le plus simple d'en créer un.

Pour des informations plus précises, lisez les types d'itérateur , l' instruction yield et les générateurs dans la documentation Python.


130

Bien que de nombreuses réponses montrent pourquoi vous utiliseriez un yieldpour créer un générateur, il existe d'autres utilisations pour yield. Il est assez facile de faire une coroutine, qui permet le passage d'informations entre deux blocs de code. Je ne répéterai aucun des beaux exemples qui ont déjà été donnés sur l'utilisation yieldpour créer un générateur.

Pour aider à comprendre ce que yieldfait le code suivant, vous pouvez utiliser votre doigt pour tracer le cycle à travers n'importe quel code qui a un yield. Chaque fois que votre doigt touche la yield, vous devez attendre un nextou sendà saisir. Quand un nextest appelé, vous tracez le code jusqu'à ce que vous frappiez le yield… le code à droite du yieldest évalué et renvoyé à l'appelant… puis vous attendez. Lorsque nextest à nouveau appelé, vous effectuez une autre boucle dans le code. Cependant, vous remarquerez que dans une coroutine, yieldpeut également être utilisé avec un send... qui enverra une valeur de l'appelant dans la fonction de rendement. Si unsend est donné, alorsyieldreçoit la valeur envoyée et la recrache du côté gauche… puis la trace à travers le code progresse jusqu'à ce que vous frappiez à était appelée).yield à nouveau (retour de la valeur à la fin, comme si next

Par exemple:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

Mignonne! Un trampoline (au sens de Lisp). On ne les voit pas souvent!
00prometheus

129

Il y a une autre yieldutilisation et signification (depuis Python 3.3):

yield from <expr>

De PEP 380 - Syntaxe de délégation à un sous-générateur :

Une syntaxe est proposée pour qu'un générateur délègue une partie de ses opérations à un autre générateur. Cela permet de factoriser une section de code contenant «yield» et de la placer dans un autre générateur. En outre, le sous-générateur est autorisé à retourner avec une valeur, et la valeur est mise à la disposition du générateur délégué.

La nouvelle syntaxe ouvre également des opportunités d'optimisation lorsqu'un générateur renvoie des valeurs produites par un autre.

De plus cela introduira (depuis Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

pour éviter que les coroutines ne soient confondues avec un générateur régulier (aujourd'hui yieldutilisé dans les deux).


117

Toutes les bonnes réponses, mais un peu difficiles pour les débutants.

Je suppose que vous avez appris la returndéclaration.

Par analogie, returnet yieldsont des jumeaux. returnsignifie «retour et arrêt» tandis que «rendement» signifie «retour, mais continue»

  1. Essayez d'obtenir une num_list avec return.
def num_list(n):
    for i in range(n):
        return i

Exécuter:

In [5]: num_list(3)
Out[5]: 0

Vous voyez, vous n'obtenez qu'un seul numéro plutôt qu'une liste d'entre eux. returnne vous permet jamais de l'emporter joyeusement, met en œuvre une seule fois et quittez.

  1. Il vient yield

Remplacez returnpar yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Maintenant, vous gagnez pour obtenir tous les chiffres.

En comparant celui returnqui s'exécute une fois et s'arrête, yieldexécute les temps que vous avez planifiés. Vous pouvez interpréter returncomme return one of them, et yieldcomme return all of them. C'est ce qu'on appelle iterable.

  1. Une étape de plus, nous pouvons réécrire la yielddéclaration avecreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

C'est l'essentiel yield.

La différence entre une returnsortie de liste et la yieldsortie d' objet est:

Vous obtiendrez toujours [0, 1, 2] à partir d'un objet de liste, mais vous ne pourrez les récupérer qu'une seule fois dans «la yieldsortie de l'objet ». Ainsi, il a un nouvel generatorobjet de nom comme affiché dans Out[11]: <generator object num_list at 0x10327c990>.

En conclusion, comme métaphore pour le dire:

  • returnet yieldsont des jumeaux
  • listet generatorsont des jumeaux

C'est compréhensible, mais une différence majeure est que vous pouvez avoir plusieurs rendements dans une fonction / méthode. L'analogie tombe totalement en panne à ce stade. Yield se souvient de sa place dans une fonction, donc la prochaine fois que vous appelez next (), votre fonction continue à la suivante yield. Ceci est important, je pense, et devrait être exprimé.
Mike S

104

Voici quelques exemples Python sur la façon d'implémenter des générateurs comme si Python ne leur fournissait pas de sucre syntaxique:

En tant que générateur Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Utilisation de fermetures lexicales au lieu de générateurs

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Utilisation de fermetures d'objets au lieu de générateurs (car ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

97

J'allais poster "lire la page 19 de" Python: référence essentielle "de Beazley pour une description rapide des générateurs", mais tant d'autres ont déjà publié de bonnes descriptions.

Notez également que cela yieldpeut être utilisé dans les coroutines comme le double de leur utilisation dans les fonctions de générateur. Bien que ce ne soit pas la même utilisation que votre extrait de code, il (yield)peut être utilisé comme expression dans une fonction. Lorsqu'un appelant envoie une valeur à la méthode à l'aide de la send()méthode, la coroutine s'exécute jusqu'à la prochaine(yield) instruction soit rencontrée.

Les générateurs et coroutines sont un moyen génial de configurer des applications de type flux de données. J'ai pensé qu'il valait la peine de connaître l'autre utilisation de l' yieldinstruction dans les fonctions.


97

Du point de vue de la programmation, les itérateurs sont implémentés comme des thunks .

Pour implémenter des itérateurs, des générateurs et des pools de threads pour une exécution simultanée, etc. en tant que thunks (également appelés fonctions anonymes), on utilise des messages envoyés à un objet de fermeture, qui a un répartiteur, et le répartiteur répond à des "messages".

http://en.wikipedia.org/wiki/Message_passing

" next " est un message envoyé à une fermeture, créé par l' appel " iter ".

Il existe de nombreuses façons d'implémenter ce calcul. J'ai utilisé la mutation, mais il est facile de le faire sans mutation, en renvoyant la valeur actuelle et le prochain rendement.

Voici une démonstration qui utilise la structure de R6RS, mais la sémantique est absolument identique à celle de Python. C'est le même modèle de calcul, et seul un changement de syntaxe est nécessaire pour le réécrire en Python.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->

84

Voici un exemple simple:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Production:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Je ne suis pas développeur Python, mais il me semble yield la position du flux de programme et la prochaine boucle commencent à partir de la position "yield". Il semble qu'il attend à cette position, et juste avant cela, renvoyant une valeur à l'extérieur, et la prochaine fois continue de fonctionner.

Cela semble être une capacité intéressante et agréable: D


Vous avez raison. Mais quel est l'effet sur le débit qui est de voir le comportement du "rendement"? Je peux changer l'algorithme au nom des mathématiques. Aidera-t-il à obtenir une évaluation différente du «rendement»?
Engin OZTURK

68

Voici une image mentale de ce qui se yieldpasse.

J'aime penser à un thread comme ayant une pile (même s'il n'est pas implémenté de cette façon).

Lorsqu'une fonction normale est appelée, elle place ses variables locales sur la pile, effectue certains calculs, puis efface la pile et retourne. Les valeurs de ses variables locales ne sont jamais revues.

Avec une yieldfonction, lorsque son code commence à s'exécuter (c'est-à-dire après que la fonction a été appelée, renvoyant un objet générateur, dont la next()méthode est ensuite invoquée), elle place également ses variables locales sur la pile et calcule pendant un certain temps. Mais ensuite, quand il frappe l' yieldinstruction, avant d'effacer sa partie de la pile et de revenir, il prend un instantané de ses variables locales et les stocke dans l'objet générateur. Il écrit également l'endroit où il se trouve actuellement dans son code (c'est-à-dire l' yieldinstruction particulière ).

C'est donc une sorte de fonction figée à laquelle le générateur est accroché.

Quand next()est appelé par la suite, il récupère les effets de la fonction sur la pile et la ré-anime. La fonction continue de calculer d'où elle s'était arrêtée, sans se rendre compte qu'elle venait de passer une éternité en chambre froide.

Comparez les exemples suivants:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Lorsque nous appelons la deuxième fonction, elle se comporte très différemment de la première. La yielddéclaration peut être inaccessible, mais si elle est présente n'importe où, elle change la nature de ce que nous traitons.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

L'appel yielderFunction()n'exécute pas son code, mais crée un générateur à partir du code. (Peut-être que c'est une bonne idée de nommer de telles choses avec le yielderpréfixe pour la lisibilité.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

Les champs gi_codeet gi_framesont l'endroit où l'état figé est stocké. En les explorant avec dir(..), nous pouvons confirmer que notre modèle mental ci-dessus est crédible.


59

Comme chaque réponse le suggère, yieldest utilisé pour créer un générateur de séquence. Il est utilisé pour générer dynamiquement une séquence. Par exemple, lors de la lecture d'un fichier ligne par ligne sur un réseau, vous pouvez utiliser la yieldfonction comme suit:

def getNextLines():
   while con.isOpen():
       yield con.read()

Vous pouvez l'utiliser dans votre code comme suit:

for line in getNextLines():
    doSomeThing(line)

Gotcha de transfert de contrôle d'exécution

Le contrôle d'exécution sera transféré de getNextLines () vers la forboucle lors de l'exécution de yield. Ainsi, chaque fois que getNextLines () est invoquée, l'exécution commence au point où elle a été interrompue la dernière fois.

Donc en bref, une fonction avec le code suivant

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

imprimera

"first time"
"second time"
"third time"
"Now some useful value 12"

59

Un exemple simple pour comprendre ce que c'est: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

La sortie est:

1 2 1 2 1 2 1 2

5
êtes-vous sûr de cette sortie? cela ne serait-il pas imprimé sur une seule ligne si vous exécutiez cette instruction d'impression en utilisant print(i, end=' ')? Sinon, je crois que le comportement par défaut mettrait chaque numéro sur une nouvelle ligne
user9074332

@ user9074332, vous avez raison, mais il est écrit sur une seule ligne pour faciliter la compréhension
Gavriel Cohen

57

(Ma réponse ci-dessous ne parle que du point de vue de l'utilisation du générateur Python, pas de l' implémentation sous - jacente du mécanisme du générateur , ce qui implique quelques astuces de manipulation de pile et de tas.)

Quand yieldest utilisé au lieu d'un returndans une fonction python, cette fonction est transformée en quelque chose de spécial appelé generator function. Cette fonction renverra un objet de generatortype. Le yieldmot-clé est un drapeau pour notifier au compilateur python de traiter une telle fonction spécialement. Les fonctions normales se termineront une fois qu'une valeur en sera retournée. Mais avec l'aide du compilateur, la fonction de générateur peut être considérée comme pouvant être reprise. Autrement dit, le contexte d'exécution sera restauré et l'exécution continuera à partir de la dernière exécution. Jusqu'à ce que vous appeliez explicitement return, ce qui déclenchera une StopIterationexception (qui fait également partie du protocole itérateur), ou atteindra la fin de la fonction. J'ai trouvé beaucoup de références generatormais cidu functional programming perspectiveest le plus digeste.

(Maintenant, je veux parler de la raison d'être generatoret de la iteratorbase de ma propre compréhension. J'espère que cela peut vous aider à saisir la motivation essentielle de l'itérateur et du générateur. Un tel concept apparaît dans d'autres langages ainsi que C #.)

Si je comprends bien, lorsque nous voulons traiter un tas de données, nous stockons généralement les données quelque part, puis les traitons une par une. Mais cette approche naïve est problématique. Si le volume de données est énorme, il est coûteux de les stocker dans leur ensemble au préalable. Donc, au lieu de stocker le datalui - même directement, pourquoi ne pas stocker une sorte de metadatamanière indirecte, c'est-à-direthe logic how the data is computed .

Il existe 2 approches pour encapsuler ces métadonnées.

  1. L'approche OO, nous enveloppons les métadonnées as a class. C'est ce que l'on appelle iteratorqui implémente le protocole itérateur (c'est-à-dire les méthodes __next__()et __iter__()). C'est également le modèle de conception d'itérateur communément observé .
  2. L'approche fonctionnelle, nous enveloppons les métadonnées as a function. C'est ce qu'on appelle generator function. Mais sous le capot, le retourné generator objectencore IS-Aiterator , car il met également en œuvre le protocole itérateur.

Quoi qu'il en soit, un itérateur est créé, c'est-à-dire un objet qui peut vous donner les données que vous souhaitez. L'approche OO peut être un peu complexe. Quoi qu'il en soit, celui à utiliser dépend de vous.


54

En résumé, l' yieldinstruction transforme votre fonction en une fabrique qui produit un objet spécial appelé a generatorqui enveloppe le corps de votre fonction d'origine. Lorsque le generatorest itéré, il exécute votre fonction jusqu'à ce qu'il atteigne la suivante, yieldpuis suspend l'exécution et évalue la valeur transmise à yield. Il répète ce processus à chaque itération jusqu'à ce que le chemin d'exécution quitte la fonction. Par exemple,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

sort simplement

one
two
three

La puissance provient de l'utilisation du générateur avec une boucle qui calcule une séquence, le générateur exécute la boucle en s'arrêtant à chaque fois pour `` produire '' le résultat suivant du calcul, de cette façon, il calcule une liste à la volée, l'avantage étant la mémoire enregistré pour des calculs particulièrement volumineux

Supposons que vous vouliez créer votre propre rangefonction qui produit une plage de nombres itérable, vous pouvez le faire comme ça,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

et l'utiliser comme ça;

for i in myRangeNaive(10):
    print i

Mais cela est inefficace car

  • Vous créez un tableau que vous n'utilisez qu'une seule fois (cela gaspille de la mémoire)
  • Ce code boucle en fait deux fois sur ce tableau! :(

Heureusement, Guido et son équipe ont été assez généreux pour développer des générateurs afin que nous puissions simplement le faire;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Maintenant, à chaque itération, une fonction sur le générateur appelé next()exécute la fonction jusqu'à ce qu'elle atteigne une instruction «yield» dans laquelle elle s'arrête et «renvoie» la valeur ou atteint la fin de la fonction. Dans ce cas, lors du premier appel, next()s'exécute jusqu'à la déclaration de rendement et donne «n», lors du prochain appel, il exécutera la déclaration d'incrémentation, reviendra au «tout», l'évaluera et, s'il est vrai, il s'arrêtera et renvoie 'n', il continuera de cette façon jusqu'à ce que la condition while renvoie false et que le générateur saute à la fin de la fonction.


53

Le rendement est un objet

Un returndans une fonction renverra une seule valeur.

Si vous souhaitez qu'une fonction renvoie un énorme ensemble de valeurs , utilisez yield.

Plus important encore, yieldest une barrière .

comme barrière dans le langage CUDA, il ne transfèrera le contrôle qu'une fois terminé.

Autrement dit, il exécutera le code dans votre fonction depuis le début jusqu'à ce qu'il frappe yield. Ensuite, il retournera la première valeur de la boucle.

Ensuite, tous les autres appels exécuteront la boucle que vous avez écrite dans la fonction une fois de plus, en retournant la valeur suivante jusqu'à ce qu'il n'y ait aucune valeur à renvoyer.


52

Beaucoup de gens utilisent returnplutôt que yield, mais dans certains cas, ils yieldpeuvent être plus efficaces et plus faciles à travailler.

Voici un exemple qui yieldest certainement le meilleur pour:

retour (en fonction)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

rendement (en fonction)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Fonctions d'appel

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Les deux fonctions font la même chose, mais yieldutilisent trois lignes au lieu de cinq et ont une variable de moins à se soucier.

C'est le résultat du code:

Production

Comme vous pouvez le voir, les deux fonctions font la même chose. La seule différence est return_dates()donne une liste et yield_dates()donne un générateur.

Un exemple concret serait quelque chose comme lire un fichier ligne par ligne ou si vous voulez simplement créer un générateur.


43

yieldest comme un élément de retour pour une fonction. La différence est que l' yieldélément transforme une fonction en générateur. Un générateur se comporte comme une fonction jusqu'à ce que quelque chose soit «cédé». Le générateur s'arrête jusqu'à son prochain appel et continue exactement au même point qu'il a commencé. Vous pouvez obtenir une séquence de toutes les valeurs «cédées» en une, en appelant list(generator()).


41

Le yieldmot clé recueille simplement les résultats renvoyés. Pensez à yieldcommereturn +=


36

Voici une yieldapproche basée simple , pour calculer la série de fibonacci, expliquée:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Lorsque vous saisissez cela dans votre REPL, puis essayez de l'appeler, vous obtenez un résultat mystifiant:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

En effet, la présence de yieldsignalés à Python que vous souhaitez créer un générateur , c'est-à-dire un objet qui génère des valeurs à la demande.

Alors, comment générez-vous ces valeurs? Cela peut être fait directement en utilisant la fonction intégrée nextou indirectement en l'alimentant dans une construction qui consomme des valeurs.

En utilisant la next()fonction intégrée, vous appelez directement .next/ __next__, forçant le générateur à produire une valeur:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Indirectement, si vous fournissez fibà une forboucle, un listinitialiseur, un tupleinitialiseur ou tout autre élément qui attend un objet qui génère / produit des valeurs, vous "consommerez" le générateur jusqu'à ce qu'il ne puisse plus produire de valeurs par lui (et il revient) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

De même, avec un tupleinitialiseur:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Un générateur diffère d'une fonction dans le sens où il est paresseux. Il accomplit cela en maintenant son état local et en vous permettant de reprendre quand vous en avez besoin.

Lorsque vous l' fibappelez pour la première fois en l'appelant:

f = fib()

Python compile la fonction, rencontre le yieldmot - clé et vous renvoie simplement un objet générateur. Il ne semble pas très utile.

Lorsque vous demandez ensuite qu'il génère la première valeur, directement ou indirectement, il exécute toutes les instructions qu'il trouve, jusqu'à ce qu'il rencontre un yield, il renvoie ensuite la valeur que vous avez fournie yieldet s'arrête. Pour un exemple qui illustre mieux cela, utilisons quelques printappels (remplacer par print "text"if sur Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Maintenant, entrez dans le REPL:

>>> gen = yielder("Hello, yield!")

vous avez maintenant un objet générateur en attente d'une commande pour générer une valeur. Utilisez nextet voyez ce qui est imprimé:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Les résultats non cotés sont ce qui est imprimé. Le résultat cité est ce qui est renvoyé yield. Appelez à nextnouveau maintenant:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Le générateur se souvient qu'il s'est arrêté yield valueet reprend à partir de là. Le message suivant est imprimé et la recherche de l' yieldinstruction pour y faire une pause a été exécutée à nouveau (en raison de la whileboucle).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.