Question Que fait le mot-clé "yield"?


Quelle est l'utilisation de la yield mot-clé en Python? Qu'est ce que ça fait?

Par exemple, j'essaie de comprendre ce code1:

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_candidates est appelé? Une liste est-elle renvoyée? Un seul élément? Est-il appelé à nouveau? Quand les appels suivants s'arrêteront-ils?


1. Le code vient de Jochen Schulz (jrschulz), qui a fait une grande bibliothèque Python pour les espaces métriques. C'est le lien vers la source complète: Module mspace.


8311
2017-10-23 22:21


origine


Réponses:


Pour comprendre ce que yield fait, vous devez comprendre ce que générateurs sont. Et avant que les générateurs viennent iterables.

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 s'appelle l'itération:

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

mylist est un itérable. Lorsque vous utilisez une liste de compréhension, 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 "for... in..."sur est une itérable; lists, strings, des dossiers...

Ces itérations 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 vous ne pouvez itérer 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 juste pareil sauf que tu as utilisé () au lieu de []. Mais toi ne peux pas effectuer for i in mygenerator une deuxième fois puisque les générateurs ne peuvent être utilisés qu'une seule fois: ils calculent 0, puis l'oublient et calculent 1, et finissent de calculer 4, un par un.

rendement

yield est un mot-clé qui est utilisé comme return, sauf la fonction va retourner 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 grand nombre de valeurs que vous n'aurez qu'à lire une seule fois.

Maîtriser yield, tu dois 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 sera exécuté chaque fois que le for utilise le générateur.

Maintenant, la partie difficile:

La première fois que for appelle l'objet générateur créé à partir de votre fonction, il va exécuter le code dans votre fonction depuis le début jusqu'à ce qu'il frappe yield, alors il retournera la première valeur de la boucle. Ensuite, chaque autre appel lancera la boucle que vous avez écrite dans la fonction une fois de plus, et retournera la valeur suivante, jusqu'à ce qu'il n'y ait plus de valeur à retourner.

Le générateur est considéré vide une fois que la fonction fonctionne, mais ne frappe pas yield plus. Cela peut être dû au fait que la boucle était terminée ou parce que vous ne satisfaisiez pas "if/else" plus.


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 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 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 candidates 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 l'itération de la boucle :-) C'est un moyen concis de parcourir toutes ces données imbriquées même si c'est un peu dangereux car on peut se retrouver avec une boucle infinie. Dans ce cas, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) épuise toutes les valeurs du générateur, mais while continue 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 noeud.

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

Habituellement, nous lui transmettons 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 parce que:

  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 dans la 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 ça marchera avec des chaînes, des listes, des tuples et des générateurs! C'est ce qu'on appelle le typage du 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, utilisezprint(corner_street_atm.__next__()) ou print(next(corner_street_atm))

Cela 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érations. Jamais souhaitez dupliquer un générateur? Chaîne deux générateurs? Grouper les valeurs dans une liste imbriquée avec un interligne? Map / Zip sans créer une autre liste?

Alors juste import itertools.

Un exemple? Voyons les commandes possibles pour une course de 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érations (implémentation de __iter__() méthode) et itérateurs (mise en œuvre du __next__() méthode). Iterables sont des objets que vous pouvez obtenir à partir d'un itérateur. Les itérateurs sont des objets qui vous permettent de parcourir des itérations.

Il y a plus à ce sujet dans cet article à propos de Comment for boucles fonctionnent.


12173
2017-10-23 22:48



Raccourci vers Grokking  yield

Lorsque vous voyez une fonction avec yield déclarations, appliquez cette astuce facile pour comprendre ce qui va arriver:

  1. Insérer une ligne result = [] au début de la fonction.
  2. Remplacer chaque yield expr avec result.append(expr).
  3. Insérer une ligne return result au bas de la fonction.
  4. Yay - pas plus yield déclarations! Lire et comprendre 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 yield est significativement différent de ce qui se passe dans l'approche basée sur la liste. Dans de nombreux cas, l'approche du rendement sera beaucoup plus efficace et plus rapide. Dans d'autres cas, cette astuce vous bloquera 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, Iterators et Generators

Premièrement la protocole d'itérateur - quand tu écris

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

Python effectue les deux étapes suivantes:

  1. Obtient un itérateur pour mylist:

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

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

  2. Utilise l'itérateur pour faire une boucle sur les éléments:

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

La vérité est que Python effectue les deux étapes ci-dessus chaque fois qu'il veut boucle sur le contenu d'un objet - donc ça pourrait être une boucle for, mais ça pourrait aussi être du code otherlist.extend(mylist) (où otherlist est une liste Python).

Ici mylist est un itérable car il implémente le protocole itérateur. Dans une classe définie par l'utilisateur, vous pouvez implémenter __iter__() méthode pour rendre les instances de votre classe itérable. Cette méthode devrait renvoyer un itérateur. Un itérateur est un objet avec un next() méthode. Il est possible de mettre en œuvre les deux __iter__() et next() sur la même classe, et ont __iter__() revenir self. Cela fonctionnera pour les cas simples, mais pas quand vous voulez deux itérateurs en boucle sur le même objet en même temps.

Donc, c'est le protocole de l'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'un for loop ne sait pas quel type d'objet il traite - il suit juste le protocole de l'itérateur, et est heureux d'obtenir élément après élément comme il appelle next(). Les listes intégrées renvoient leurs éléments un par un, les dictionnaires clés un par un, les fichiers renvoient le lignes un par un, etc. Et les générateurs reviennent ... eh bien c'est là yield entre:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Au lieu de yield déclarations, si vous aviez trois return déclarations dans f123() seulement le premier serait exécuté, et la fonction sortirait. Mais f123() n'est pas une fonction ordinaire. Quand f123() est appelé, il ne fait pas retourner l'une des valeurs dans les déclarations de rendement! Il renvoie un objet générateur. De plus, la fonction ne sort pas vraiment - elle passe à l'état suspendu. Quand le for boucle essaie de faire une boucle sur l'objet générateur, la fonction reprend de son état suspendu à la ligne suivante après le yield il a précédemment renvoyé à partir de, exécute la ligne de code suivante, dans ce cas un yield déclaration, et renvoie cela comme l'élément suivant. Cela se produit jusqu'à ce que la fonction se termine, à quel point le générateur soulève StopIterationet la boucle se termine.

Ainsi, l'objet générateur est un peu comme un adaptateur - à une extrémité, il présente le protocole de l'itérateur, en exposant __iter__() et next() méthodes pour garder le for boucle 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 qui implémente la même logique. Une option consiste à utiliser l'astuce de la liste temporaire que j'ai mentionnée auparavant. Cela ne fonctionnera pas dans tous les cas, par exemple. si vous avez des boucles infinies, ou si votre liste est vraiment longue, cela peut rendre inefficace l'utilisation de la mémoire. L'autre approche consiste à implémenter une nouvelle classe itérable SomethingIter qui maintient l'état dans les membres d'instance et effectue la prochaine étape logique dans son next() (ou __next__() en Python 3) méthode. Selon la logique, le code à l'intérieur du next() méthode peut finir par regarder très complexe et être sujette à des bugs. Ici, les générateurs fournissent une solution propre et facile.


1635
2017-10-25 21:22



Pense-y de cette façon:

Un itérateur est juste un terme de fantaisie pour un objet qui a une méthode next (). Donc une fonction de rendement finit par être quelque chose comme ça:

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, le for boucle peut être réécrit à ceci:

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

Cela a-t-il plus de sens ou vous rend plus confus? :)

Je devrais noter que ceci est une simplification excessive à des fins d'illustration. :)


396
2017-10-23 22:28



le yield mot-clé est réduit à deux faits simples:

  1. Si le compilateur détecte le yield mot-clé nulle part à l'intérieur d'une fonction, cette fonction ne retourne plus via le return déclaration. Au lieu, il immédiatement retourne un objet paresseux "en attente de liste" appelé un générateur
  2. Un générateur est itérable. Qu'est-ce qu'un itérable? C'est quelque chose comme un list ou set ou range ou dict-view, avec un protocole intégré pour visiter chaque élément dans un certain ordre.

En un mot: un générateur est une liste paresseuse, incrémentielle en attente, et yield les instructions vous permettent d'utiliser la notation de fonction pour programmer les valeurs de la liste le générateur devrait cracher progressivement.

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 makeRange c'est comme Python range. Appel 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 à retourner immédiatement ses valeurs en attente, vous pouvez le transmettre list() (Juste comme vous pourriez tout itérable):

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

Comparer l'exemple à "juste retourner une liste"

L'exemple ci-dessus peut être considéré comme une simple création d'une liste à laquelle 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 y a une différence majeure, cependant; voir la dernière section.


Comment vous pourriez utiliser des générateurs

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

#                   _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 itertools module (assurez-vous d'utiliser chain.from_iterable plutôt que chain lorsque 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 pourriez implémenter votre propre def enumerate(iterable): zip(count(), iterable), ou alternativement le faire avec yieldmot-clé dans une boucle while.

S'il vous plaît noter: les générateurs peuvent effectivement être utilisés pour beaucoup plus de choses, telles que mettre en œuvre des coroutines ou 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". Autrement dit, que se passe-t-il lorsque vous faites list(makeRange(5)). C'est ce que je décris plus haut comme une «liste paresseuse et incrémentale».

>>> 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 les objets .next() fonction, qui fait partie du "protocole d'itération" et se trouve sur tous les itérateurs. Vous pouvez utiliser manuellement le next() fonction (et d'autres parties du protocole d'itération) pour mettre en œuvre des choses de fantaisie, généralement au détriment de la lisibilité, alors essayez d'éviter cela ...


Menus détails

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

En langage Python, un itérable est un 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 demande pour boucle comme [1,2,3].__iter__(). UNE Générateur est exactement le même que n'importe quel itérateur, sauf pour la façon dont il a été écrit (avec la syntaxe de la 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 d'un itérateur (ce que vous feriez rarement), il vous donne juste une copie de lui-même.

Ainsi, dans le cas improbable où vous ne parvenez pas à faire quelque chose comme ça ...

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

... alors rappelez-vous qu'un générateur est un itérateur; c'est-à-dire, c'est une utilisation unique. Si vous voulez le réutiliser, vous devriez appeler myRange(...) encore. 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 font une métaprogrammation terrifiante) peuvent utiliser itertools.tee si absolument nécessaire, puisque l'itérateur copiable Python DYNAMISME proposition de normes a été différée.


346
2018-06-19 06:33



Que fait le yield mot-clé faire en Python?

Réponse Sommaire / Résumé

  • Une fonction avec yield, quand appelé, retourne un Générateur.
  • Les générateurs sont des itérateurs parce qu'ils mettent en œuvre protocole d'itérateur, donc vous pouvez itérer sur eux.
  • Un générateur peut aussi être informations envoyées, ce qui en fait un conceptuellement coroutine.
  • En Python 3, vous pouvez déléguer d'un générateur à l'autre dans les deux sens avec yield from.
  • (L'annexe critique quelques réponses, y compris la première, et traite de l'utilisation de return dans un générateur.)

Générateurs:

yield est seulement légal à l'intérieur d'une définition de fonction, et l'inclusion de yield dans une définition de fonction, il renvoie un générateur.

L'idée des générateurs vient d'autres langues (voir note de bas de page 1) avec des implémentations variées. Dans les générateurs de Python, l'exécution du code est congelé au point du rendement. Lorsque le générateur est appelé (les méthodes sont discutées ci-dessous), l'exécution reprend et se fige au rendement suivant.

yield fournit un moyen facile de 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 faire un objet un itérateur que vous pourriez taper-vérifier avec le Iterator Base abstraite Classe de la collections module.

>>> 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 taper comme ceci:

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

Une caractéristique d'un Iterator  est-ce 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 faire un autre si vous voulez réutiliser ses fonctionnalités (voir note de bas de page 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 à ci-dessous - à partir de Python 3.3 (et non disponible en Python 2), vous pouvez utiliser yield from:

def func(an_iterable):
    yield from an_iterable

cependant, yield from permet également la délégation aux sous-générateurs, qui sera expliqué dans la section suivante sur la délégation coopérative avec des sous-corotines.

Coroutines:

yield forme une expression qui permet l'envoi de données dans le générateur (voir référence 3)

Voici un exemple, prenez note du received variable, 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, nous devons mettre en file d'attente le générateur avec la fonction intégrée, next. Ce sera appeler le approprié next ou __next__ méthode, en fonction de 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. (Envoi None est la même chose que d'appeler next.):

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

Délégation coopérative à la sous-corpo- yield from

Maintenant, rappelez-vous que yield from est disponible en Python 3. Cela nous permet de déléguer coroutines à une sous-routine:

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 de yield from dans PEP 380.

Autres méthodes: fermer et jeter

le close méthode soulève GeneratorExit au point la fonction l'exécution a été gelée. Cela sera également appelé par __del__ donc tu peut mettre un code de nettoyage où vous gérez le GeneratorExit:

>>> my_account.close()

Vous pouvez également lancer une exception qui peut être gérée dans le générateur ou propagé à 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 crois avoir couvert tous les aspects de la question suivante:

Que fait le yield mot-clé faire en Python?

Il se trouve que yield fait beaucoup. Je suis sûr que je pourrais ajouter encore plus des exemples approfondis à cela. Si vous voulez plus ou avoir une critique constructive, faites le moi savoir en commentant au dessous de.


Annexe:

Critique de la réponse supérieure / acceptée **

  • Il est confus sur ce qui fait 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 un __iter__ méthode renvoyant un itérateur. Un itérateur fournit un .next (Python 2 ou .__next__ (Python 3), implicitement appelée par forboucles jusqu'à ce qu'il soulève StopIterationet une fois qu'il le fera, il continuera à le faire.
  • Il utilise ensuite une expression de générateur pour décrire ce qu'est un générateur. Depuis un générateur est simplement un moyen pratique de créer un itérateur, cela ne fait que créer de la confusion, et nous ne sommes pas encore arrivés au yield partie.
  • Dans Contrôler l'épuisement d'un générateur il appelle le .next méthode, quand il devrait plutôt utiliser la fonction intégrée, next. Ce serait une couche d'indirection appropriée, car son code ne fonctionne pas dans Python 3.
  • Itertools? Ce n'était pas pertinent pour yield fait du tout.
  • Pas de discussion sur les méthodes qui yield fournit avec la nouvelle fonctionnalité yield from en Python 3. La réponse supérieure / acceptée est une réponse très incomplète.

Critique de la réponse suggérant yield dans une expression ou une compréhension de générateur.

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

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

Puisque le rendement est une expression, certains l'ont vanté comme intéressant de l'utiliser dans les expressions de compréhension ou de générateur - en dépit de citer aucun cas d'utilisation particulièrement bon.

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

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

Le dim. 29 janv. 2017 à 16:39 Craig Rodrigues a écrit:

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

Mon vote est-il un SyntaxError puisque vous n'obtenez pas ce que vous attendez de     la syntaxe.

Je suis d'accord que c'est un endroit sensible pour nous de finir, comme tout code   compter sur le comportement actuel est vraiment trop intelligent pour être   maintenable.

Pour ce qui est d'y arriver, nous voudrons probablement:

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

À la vôtre, Nick.

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

De plus, il y a un question en suspens (10544) qui semble pointer dans la direction de cette jamais c'est une bonne idée (PyPy, une implémentation Python écrite en Python, soulève déjà des avertissements de syntaxe.)

Bottom line, jusqu'à ce que les développeurs de CPython nous disent le contraire: Ne pas mettre yield dans une expression ou une compréhension de générateur.

le return déclaration dans un générateur

Dans Python 2:

Dans une fonction de générateur, le return déclaration n'est pas autorisé à inclure un expression_list. Dans ce contexte, un nu return indique que le générateur est fait et provoquera StopIteration être élevé.

Un expression_list est fondamentalement n'importe quel nombre d'expressions séparées par des virgules - essentiellement, dans Python 2, vous pouvez arrêter le générateur avec return, mais vous ne pouvez pas retourner une valeur.

Dans Python 3:

Dans une fonction de générateur, le return déclaration indique que le générateur est fait et provoquera StopIteration être élevé. La valeur retournée (le cas échéant) est utilisée comme argument pour construire StopIteration et devient le StopIteration.valueattribut.

Notes de bas de page

  1. Les langues CLU, Sather et Icon ont été référencées dans la proposition présenter le concept de générateurs à Python. L'idée générale est qu'une fonction peut maintenir l'état interne et le rendement intermédiaire points de données à la demande de l'utilisateur. Cela a promis d'être supérieur en performance à 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 xrange objets (range en Python 3) ne sont pas Iterators, même si elles sont itératives, car elles peuvent être réutilisées. Comme les listes, leur __iter__ méthodes retournent des objets d'itérateur.

  3. yield a été initialement présenté comme une déclaration, ce qui signifie qu'il ne pouvait apparaître qu'au début d'une ligne dans un bloc de code. À présent yield crée une expression de rendement. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt  Ce changement était proposé pour permettre à un utilisateur d'envoyer des données dans le générateur tout comme on pourrait le recevoir. Pour envoyer des données, il faut pouvoir l'assigner à quelque chose, et pour cela, une déclaration ne fonctionnera tout simplement pas.


253
2018-06-25 06:11



yield est juste 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 à la yield déclaration. Contrairement au retour, le cadre de la pile n'est pas nettoyé lorsqu'un rendement se produit, mais le contrôle est retransféré à l'appelant, de sorte que son état reprendra la prochaine fois que la fonction sera rétablie.

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

list.extend appelle un itérateur jusqu'à ce qu'il soit épuisé. Dans le cas de l'exemple de code que vous avez posté, il serait beaucoup plus clair de simplement retourner un tuple et l'ajouter à la liste.


230
2017-10-23 22:24



Il y a une chose supplémentaire à mentionner: une fonction qui ne doit pas nécessairement 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 rend certaines choses plus faciles à travailler.


182
2017-10-24 08:44



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

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

155
2018-01-18 17:25



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, foo conserve la liste entière en mémoire à la fois. Ce n'est pas un gros problème pour une liste avec 5 éléments, mais que faire si vous voulez une liste de 5 millions? Non seulement c'est un mangeur de mémoire énorme, mais cela prend aussi beaucoup de temps à construire au moment où la fonction est appelée. Dans le second cas, la barre vous donne juste un générateur. Un générateur est un itérable - ce qui signifie que vous pouvez l'utiliser dans une boucle for, etc, mais chaque valeur ne peut être accessible qu'une seule fois. Toutes les valeurs ne sont pas stockées en mémoire en même temps; l'objet générateur "se souvient" où il était dans la boucle la dernière fois que vous l'appeliez - de cette façon, si vous utilisez un itérable pour (disons) compter jusqu'à 50 milliards, vous ne devez pas compter jusqu'à 50 milliards tous à la fois et stocker les 50 milliards de numéros à compter à travers. Encore une fois, c'est un exemple assez artificiel, vous utiliseriez probablement itertools si vous vouliez vraiment compter jusqu'à 50 milliards. :)

C'est le 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 le rendement pour faire avancer 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 la traversée d'arbres spécialisés et toutes sortes d'autres choses.


133
2018-01-16 06:42



Il retourne un générateur. Je ne suis pas particulièrement familier avec Python, mais je crois que c'est le même genre de chose que Blocs d'itération de C # si vous êtes familier avec ceux-ci.

Il y a un Article d'IBM ce qui explique assez bien (pour Python) autant que je peux voir.

L'idée clé est que le compilateur / interpréteur / quoi que ce soit fasse des trucs pour que l'appelant puisse continuer à appeler next () et qu'il continue de renvoyer des valeurs - comme si la méthode du générateur était en pause. Maintenant, évidemment, vous ne pouvez pas vraiment "mettre en pause" une méthode, donc le compilateur construit une machine à états pour vous rappeler où vous êtes actuellement et à quoi ressemblent les variables locales etc. C'est beaucoup plus facile que d'écrire un itérateur par vous-même.


125
2017-10-23 22:26



Il y a un type de réponse qui, selon moi, n'a pas encore été donnée, parmi les nombreuses bonnes réponses qui décrivent comment utiliser les générateurs. Voici la réponse à la théorie du langage de programmation:

le yield instruction en Python renvoie un générateur. Un générateur en Python est une fonction qui renvoie continuations (et spécifiquement un type de coroutine, mais les continuations représentent le mécanisme 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 très difficiles à mettre en œuvre. Mais l'idée de ce qu'est une continuation est simple: c'est l'état d'un calcul qui n'est pas encore fini. Dans cet état, les valeurs actuelles des variables, les opérations à effectuer et ainsi de suite sont enregistrées. Ensuite, à un moment donné dans le programme, la continuation peut être invoquée, de sorte que les variables du programme sont réinitialisées à cet état et les opérations qui ont été sauvegardées sont effectuées.

Les continuations, sous cette forme plus générale, peuvent être mises en œuvre de deux manières. dans le call/cc Ainsi, la pile du programme est littéralement sauvegardée et lorsque la continuation est invoquée, la pile est restaurée.

Dans Continuing Passing Style (CPS), les continuations ne sont que des fonctions normales (uniquement dans les langages dont les fonctions sont de première classe) que le programmeur gère explicitement et passe aux sous-routines. 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 en tant qu'arguments (dans certaines variantes de CPS, les fonctions peuvent accepter plusieurs suites) et manipulent le flux de contrôle en les invoquant simplement en les appelant et en revenant ensuite. Un exemple très simple de continuation passing style 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 sauve l'opération d'écriture réelle du fichier dans une continuation (qui peut être une opération très complexe avec beaucoup de détails à écrire), et passe ensuite cette continuation (c'est-à-dire, en premier 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 sauve 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 ce post va, sans perte de généralité, conceptualiser les continuations comme CPS, car c'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. Tandis que les poursuites sont en général en mesure de sauver l'état d'un calcul (c'est-à-dire, la pile d'appels du programme), les générateurs sont seulement en mesure de sauvegarder l'état de l'itération sur un itérateur. Bien que, cette définition est légèrement trompeuse pour certains cas d'utilisation de générateurs. Par exemple:

def f():
  while True:
    yield 4

C'est clairement un itérable raisonnable dont le comportement est bien défini - chaque fois que le générateur l'itéte, il renvoie 4 (et le fait pour toujours). Mais ce n'est probablement pas le type prototypique de l'itération qui vient à l'esprit lorsqu'on pense aux itérateurs (c.-à-d. for x in collection: do_something(x)). 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 suites peuvent sauvegarder l'état de la pile d'un programme et les générateurs peuvent sauvegarder l'état d'itération. Cela signifie que les suites sont plus puissantes que les générateurs, mais aussi que les générateurs sont beaucoup plus faciles. Ils sont plus faciles à mettre en œuvre pour le concepteur de langage, et ils sont plus faciles à utiliser pour le programmeur (si vous avez du temps à brûler, essayez de lire et de comprendre cette page sur les continuations et appel / cc).

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

N'importe quand yield est appelé, il dit à la fonction de retourner une continuation. Lorsque la fonction est appelée à nouveau, elle commence là où elle s'est arrêtée. Donc, en pseudo-pseudo-code (c'est-à-dire, pas de pseudo-code, mais pas de code), le générateur next La méthode 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 yield mot-clé est en fait du sucre syntaxique pour la fonction de générateur réel, fondamentalement quelque chose comme:

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

Souvenez-vous que ceci n'est que pseudocode et que l'implémentation réelle des générateurs en Python est plus complexe. Mais comme un exercice pour comprendre ce qui se passe, essayez d'utiliser le style de passe continu pour implémenter des objets générateurs sans utiliser le yield mot-clé.


120
2018-04-04 14:56