web-dev-qa-db-fra.com

Que fait le mot clé "rendement"?

Quelle est l'utilisation du mot clé yield 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ée? Une liste est-elle renvoyée? Un seul élément? Est-il appelé à nouveau? Quand les appels suivants vont-ils s'arrêter?


1. Le code provient de Jochen Schulz (jrschulz), qui a créé une excellente bibliothèque Python pour les espaces métriques. Voici le lien vers la source complète: Module mspace .

8943
Alex. S.

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

Iterables

Lorsque vous créez une liste, vous pouvez lire ses éléments un à un. La lecture de ses éléments un à un s'appelle itération:

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

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

>>> 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..." est un itératif; 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 lorsque vous avez beaucoup de valeurs.

Générateurs

Les générateurs sont des itérateurs, une sorte d'iterable que vous ne pouvez parcourir qu'une 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é () au lieu de []. MAIS, vous ne pouvez pas exécuter for i in mygenerator une seconde fois, car les générateurs ne peuvent être utilisés qu’une fois: ils calculent 0, puis l’oublient et calculent 1, et terminent le calcul de 4, un par un.

Rendement

yield est un mot clé utilisé comme return, sauf que la fonction renvoie 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 très grand nombre de valeurs que vous ne devrez lire qu’une 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 compliqué :-)

Ensuite, votre code continuera d’où il s’est arrêté chaque fois que for utilise le générateur.

Maintenant la partie difficile:

La première fois que la variable for appelle l'objet générateur créé à partir de votre fonction, le code de votre fonction sera exécuté depuis le début jusqu'à ce qu'il atteigne la variable yield, puis il renverra la première valeur de la boucle. Ensuite, chaque appel exécutera à nouveau la boucle que vous avez écrite dans la fonction et renverra la valeur suivante, jusqu'à ce qu'il n'y ait plus de valeur à renvoyer.

Le générateur est considéré comme vide une fois la fonction exécutée, mais ne touche plus yield. Cela peut être dû à la fin de la boucle ou à la non satisfaction d'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 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 que la boucle est itérée :-) C'est une façon concise 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)) épuise toutes les valeurs du générateur, mais while continue à créer de nouveaux objets générateur qui produiront des valeurs différentes des précédentes car elles ne sont pas appliquées sur le même nœud.

  • La méthode extend() est une méthode d'objet de liste qui attend un itératif 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 y a 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 en mémoire.

Et cela fonctionne parce que Python ne se soucie pas de savoir si l'argument d'une méthode est une liste ou non. Python attend des iterables, donc il fonctionnera avec des chaînes, des listes, des n-uplets et des générateurs! Cela s’appelle dactylographie 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 iterables. Avez-vous déjà souhaité dupliquer un générateur? Chaîne de deux générateurs? Grouper les valeurs dans une liste imbriquée avec un one-liner? Map / Zip sans créer une autre liste?

Alors juste import itertools.

Un exemple? Voyons les ordres d'arrivée 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érables (implémentation de la méthode __iter__()) et des itérateurs (implémentant la méthode __next__()) . Les itérateurs sont des objets qui vous permettent d'itérer sur des itérables.

Il y en a plus dans cet article à propos de comment les boucles for fonctionnent .

13125
e-satis

Raccourci vers Grokkingyield

Lorsque vous voyez une fonction avec des instructions yield, appliquez cette astuce simple pour comprendre ce qui va se passer:

  1. Insérer une ligne result = [] au début de la fonction.
  2. Remplacez chaque yield expr par result.append(expr).
  3. Insérer une ligne return result au bas de la fonction.
  4. Yay - pas plus de yield déclarations! Lire et comprendre le code.
  5. Comparez la fonction à la définition originale.

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 très différent de ce qui se passe dans l’approche par liste. Dans de nombreux cas, l’approche de rendement sera beaucoup plus efficace en termes de mémoire et plus rapide. Dans d'autres cas, cette astuce vous bloquera dans une boucle infinie, même si la fonction d'origine fonctionne parfaitement. Continuez à lire pour en savoir plus...

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

Tout d'abord, le protocole iterator - lorsque vous écrivez

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

Python effectue les deux étapes suivantes:

  1. Obtient un itérateur pour mylist:

    Call iter(mylist) -> Ceci retourne un objet avec une méthode next() (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 la méthode next() sur l'itérateur renvoyé à l'étape 1. La valeur renvoyée par next() est affectée à x et le corps de la boucle est exécuté. Si une exception StopIteration est générée à l'intérieur de next(), cela signifie qu'il n'y a plus de valeurs dans l'itérateur et que la boucle est sortie.

En réalité, Python effectue les deux étapes ci-dessus chaque fois qu'il le souhaite boucle terminée le contenu d'un objet. Il peut donc s'agir d'une boucle for, mais il peut également s'agir d'un code comme otherlist.extend(mylist) (où otherlist est une liste Python ).

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

C'est donc le protocole itérateur, de nombreux objets implémentent ce protocole:

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

Notez qu'une boucle for ne sait pas quel type d'objet elle traite - elle ne fait que suivre le protocole itérateur, et est heureuse d'obtenir élément après élément car elle appelle next(). Les listes intégrées renvoient leurs éléments un par un, les dictionnaires renvoient les clés un par un, les fichiers renvoient les lignes un par un, etc. Et les générateurs reviennent ... yield entre:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Au lieu des instructions yield, si vous aviez trois instructions return dans f123(), seul le premier serait exécuté et la fonction se fermerait. Mais f123() n’est pas une fonction ordinaire. Lorsque f123() est appelé, il ne le fait pas ne renvoie aucune des valeurs des déclarations de rendement! Il retourne un objet générateur. En outre, la fonction ne quitte pas vraiment - elle passe dans un état suspendu. Lorsque la boucle for tente de parcourir l'objet générateur, la fonction reprend son état suspendu à la ligne suivante après la yield à partir de laquelle elle est retournée, exécute la ligne suivante de code, dans ce cas une instruction yield, et la renvoie sous la forme suivante: l'élément suivant. Cela se produit jusqu'à la sortie de la fonction, moment auquel le générateur lève StopIteration et la boucle se ferme. 

Ainsi, l'objet générateur ressemble à un adaptateur: à une extrémité, il présente le protocole d'itérateur, en exposant les méthodes __iter__() et next() pour que la boucle for reste heureuse. Cependant, à l’autre extrémité, 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?

Généralement, 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 "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 que vous aurez une utilisation inefficace de la mémoire si vous avez une très longue liste. L'autre approche consiste à implémenter une nouvelle classe itérative SomethingIter qui conserve l'état dans les membres de l'instance et effectue l'étape logique suivante dans sa méthode next() (ou __next__() en Python 3). Selon la logique, le code dans la méthode next() peut paraître très complexe et être sujet à des bogues. Ici, les générateurs fournissent une solution propre et facile.

1751
user28409

Pense-y de cette façon:

Un itérateur est juste un terme de qualité pour un objet qui a une méthode next (). 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 l’interpréteur Python fait 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 boucle for peut être réécrite comme suit:

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

Cela a-t-il plus de sens ou vous perturbe-t-il davantage? :)

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

445
Jason Baker

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

  1. Si le compilateur détecte le mot clé yieldn'importe où à l'intérieur d'une fonction, cette fonction ne sera plus renvoyée via l'instruction return. À la place, il immédiatement renvoie un objet lazy "liste en attente" appelé un générateur
  2. Un générateur est itérable. Qu'est-ce qu'un iterable? C'est quelque chose comme une list ou set ou range ou dict-view, avec un protocole intégré permettant de visiter chaque élément dans un certain ordre.

En un mot: un générateur est une liste en attente par incrément de manière incrémentielle, et les instructions yield 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 makeRange qui ressemble à range de Python. 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 transférer dans list() (comme vous pouvez le faire avec une variable quelconque):

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

Comparaison exemple à "renvoyer juste une liste"

L’exemple ci-dessus peut être considéré comme une simple création d’une liste à ajouter et à renvoyer:

# 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 cependant une différence majeure. voir la dernière section.


Comment utiliser des générateurs

Un itérable est la dernière partie d'une liste de compréhension, 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 module itertools (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 telles que itertools.count(). Vous pouvez implémenter votre propre def enumerate(iterable): Zip(count(), iterable) ou le faire avec le mot clé yield dans une boucle while.

Remarque: les générateurs peuvent en réalité être utilisés pour beaucoup d'autres choses, telles que la mise en œuvre de coroutines ou une 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

Voici comment fonctionne le "protocole d'itération Python". C'est-à-dire, que se passe-t-il quand vous faites list(makeRange(5)). C'est ce que je décris plus tôt comme une "liste incrémentale 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 fonction objects .next(), qui fait partie du "protocole d'itération" et se trouve sur tous les itérateurs. Vous pouvez utiliser manuellement la fonction next() (ainsi que d'autres parties du protocole d'itération) pour implémenter des éléments fantaisistes, généralement au détriment de la lisibilité. Essayez donc d'éviter de le faire ...


Menus détails

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

En Python, un iterable 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 requête for- boucle comme [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 la fonction).

Lorsque vous demandez un itérateur dans une liste, un nouvel itérateur est créé. Cependant, lorsque vous demandez un itérateur à un itérateur (ce que vous feriez rarement), il vous en donne simplement une copie.

Ainsi, dans le cas improbable où vous ne feriez pas 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 qu'il s'agit d'un usage unique. Si vous souhaitez le réutiliser, vous devez appeler à nouveau myRange(...). Si vous devez utiliser le résultat deux fois, convertissez-le 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 cela est absolument nécessaire, puisque la proposition en copie de Python PEP en copie a été différée.

380
ninjagecko

yield est comme return - il retourne 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'instruction yield. Contrairement à return, le cadre de la pile n’est pas nettoyé lorsqu’un rendement se produit, mais le contrôle est transféré à l’appelant. Son état est rétabli lors du prochain appel de la fonction.

Dans le cas de votre code, la fonction get_child_candidates agit comme un itérateur. Ainsi, lorsque vous étendez votre liste, elle 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 renvoyer un tuple et de l'ajouter à la liste.

260
Douglas Mayle

Il y a une chose supplémentaire à mentionner: une fonction qui rend ne doit pas réellement se terminer. J'ai écrit un 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 à gérer. 

199
Claudiu

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

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

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

faire ceci:

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

Chaque fois que vous vous créez une liste à partir de rien, yield chaque pièce à la place. 

Ce fut mon premier moment "aha" avec rendement.


yield est un sucré moyen de dire 

construire une série de choses

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 single-pass: vous ne pouvez parcourir qu'une seule fois. Quand une fonction a un rendement, on appelle cela une fonction generator . Et un itérateur est ce qu’il retourne. Ces termes sont révélateurs. Nous perdons la commodité d'un conteneur, mais bénéficions de la puissance d'une série calculée au besoin et arbitrairement longue.

Le rendement est lazy, cela retarde le calcul. Une fonction avec un rendement dedans ne s'exécute pas du tout lorsque vous l'appelez. Elle renvoie un objet iterator qui se souvient de l'endroit où il s'était arrêté. Chaque fois que vous appelez next() sur l'itérateur (cela se produit dans une boucle for), l'exécution avance petit à petit au prochain rendement. return lè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 multiple passes et que la série ne soit pas trop longue, appelez simplement list():

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

Choix génial du mot yield parce que les deux significations 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 dans le pouvoir politique)

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

160
Bob Stein

À mon avis, il n’ya pas encore eu de type de réponse parmi les nombreuses excellentes qui décrivent comment utiliser des générateurs. Voici la réponse de la théorie du langage de programmation:

L'instruction yield dans Python renvoie un générateur. Un générateur dans Python est une fonction qui retourne les 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 suites 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 terminé. Dans cet état, les valeurs actuelles des variables, les opérations non encore exécutées, etc., sont enregistrées. Ensuite, à un moment ultérieur du programme, la suite peut être appelée, de sorte que les variables du programme sont réinitialisées à cet état et que les opérations sauvegardées sont exécutées.

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

Dans la suite passant style (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 transfère 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 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 y retournant 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 consistant à écrire le fichier dans une continuation (ce qui peut être potentiellement une opération très complexe avec beaucoup de détails à écrire), puis passe cette continuation (c'est-à-dire en tant que 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 d'interface graphique, soit parce qu'il m'enregistre des lignes de code, soit, plus important encore, pour gérer le flux de contrôle après le déclenchement d'événements d'interface graphique.)

Le reste de cet article va, sans perte de généralité, conceptualiser les suites en tant que CPS, car il est extrêmement 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 peuvent en général sauvegarder l'état d'un calcul (c'est-à-dire, la pile d'appels du programme ), les générateurs ne peuvent sauvegarder l’état d’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

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

Pour réitérer: les continuations peuvent sauvegarder l'état de la pile d'un programme et les générateurs peuvent sauvegarder l'état de l'itération. Cela signifie que les continuations sont beaucoup plus puissantes que les générateurs, mais également que les générateurs sont beaucoup plus faciles. Celles-ci sont plus faciles à implémenter par 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 continuations et call/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 que yield est appelé, il indique à la fonction de renvoyer une suite. Lorsque la fonction est appelée à nouveau, elle commence où qu'elle se soit arrêtée. Ainsi, en pseudo-pseudo-code (c'est-à-dire pas pseudo-code, mais pas code), la méthode next 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 mot clé yield est en réalité un sucre syntaxique pour la fonction de générateur réelle, ce qui revient à dire:

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

Rappelez-vous qu'il ne s'agit que d'un pseudocode et que la mise en œuvre réelle des générateurs dans 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 mot clé yield.

149
aestrivex

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 constater, dans le premier cas, foo conserve la liste complète en mémoire en une fois. Ce n'est pas un gros problème pour une liste à 5 éléments, mais si vous voulez une liste de 5 millions? Non seulement c'est un gros mangeur de mémoire, mais sa construction coûte beaucoup de temps au moment où la fonction est appelée. Dans le second cas, bar vous donne simplement un générateur. Un générateur est un itératif - ce qui signifie que vous pouvez l'utiliser dans une boucle for, etc., mais chaque valeur n'est accessible qu'une seule fois. De plus, toutes les valeurs ne sont pas 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 nombre itérable pour (par exemple) compter jusqu'à 50 milliards, vous n'avez pas à compter jusqu'à 50 milliards à la fois et stocker les 50 milliards de chiffres à compter. Encore une fois, c’est un bel exemple 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 pousser des 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, entre autres choses.

148
RBansal

Il retourne un générateur. Je ne connais pas très bien Python, mais je pense que c'est le même genre de chose que les blocs d'itérateurs de C # si vous les connaissez bien.

L'idée clé est que le compilateur/interprète/quoi que ce soit fasse quelques ruses pour que l'appelant puisse continuer à appeler next () et qu'il continue à 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, aussi le compilateur construit-il une machine à états pour que vous puissiez vous rappeler où vous en êtes et à quoi ressemblent les variables locales, etc. C'est beaucoup plus facile que d'écrire un itérateur vous-même.

145
Jon Skeet

Voici un exemple en langage clair. Je ferai correspondre les concepts humains de haut niveau aux concepts Python de bas niveau.

Je veux opérer sur une séquence de nombres, mais je ne veux pas me déranger 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 produite de manière spécifique et que je vous dise quel est l'algorithme. 
    Cette étape correspond à defde la fonction du générateur, c’est-à-dire à la fonction contenant un yield.
  • Quelque temps plus tard, je vous dis: "OK, prépare-toi à me dire la suite 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 dites pas encore de chiffres; vous venez de prendre votre papier et un crayon.
  • Je vous demande, "dites-moi le prochain numéro", et vous me dites le premier numéro; après cela, vous attendez que je vous demande le numéro suivant. C'est votre travail de vous rappeler où vous étiez, quels numéros vous avez déjà dit et quel est le prochain numéro. Je me fiche des détails. 
    Cette étape correspond à l'appel de .next() sur l'objet générateur.
  • … Répéter l'étape précédente jusqu'à…
  • finalement, vous pourriez finir. Tu ne me dis pas de numéro; vous venez de crier: "Tenez vos chevaux! J'ai fini! Plus de chiffres!" 
    Cette étape correspond à l’objet générateur qui termine son travail et déclenche une exception StopIteration La fonction de générateur n'a pas besoin de déclencher l'exception. Il est déclenché automatiquement lorsque la fonction se termine ou émet une return.

C’est ce que fait un générateur (une fonction qui contient une yield); il commence à s'exécuter, met en pause chaque fois qu'il fait une variable yield, et lorsqu'on lui demande une valeur .next(), il continue à partir du point où il se trouvait la dernière fois. Il s’intègre parfaitement de par sa conception au protocole itérateur de Python, qui explique comment demander des valeurs de manière séquentielle.

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

for item in sequence:

peu importe si sequence est une liste, une chaîne, un dictionnaire ou un générateur object comme décrit ci-dessus; le résultat est le même: vous lisez les éléments d'une séquence, l'un après l'autre.

Notez que definfigurer une fonction contenant un mot clé yield n’est pas le seul moyen de créer un générateur; c'est simplement le moyen le plus simple d'en créer un.

Pour des informations plus précises, consultez types d'itérateurs , les instructions de rendement et générateurs de la documentation Python.

120
tzot

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

Pour vous aider à comprendre ce que fait une yield dans le code suivant, vous pouvez utiliser votre doigt pour suivre le cycle dans tout code comportant un yield. Chaque fois que votre doigt touche la variable yield, vous devez attendre la saisie d'une variable next ou send. Quand une next est appelée, vous suivez le code jusqu'à ce que vous ayez appuyé sur la yield… le code situé à droite de la yield soit évalué et renvoyé à l'appelant… puis vous attendez. Lorsque next est appelé à nouveau, vous effectuez une autre boucle dans le code. Cependant, vous remarquerez que, dans une coroutine, yield peut également être utilisé avec un send… qui enverra une valeur de l'appelant dans la fonction de cession. Si send est donné, alors yield reçoit la valeur envoyée et la recrache du côté gauche… puis la trace dans le code progresse jusqu'à ce que vous atteigniez de nouveau la yield (renvoyant la valeur à la fin, comme si next était appelé).

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()
105
Mike McKerns

Il existe une autre utilisation yield et sa signification (depuis Python 3.3):

yield from <expr>

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

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

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

De plus this introduira (depuis Python 3.5):

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

éviter que les routines ne soient confondues avec un générateur régulier (on utilise aujourd'hui yield dans les deux cas).

97
Sławomir Lenart

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

Je suppose que vous avez appris la déclaration return.

Par analogie, return et yield sont des jumeaux. return signifie 'retour et arrêt' alors que 'rendement' signifie 'retour, mais continue'

  1. Essayez d’obtenir une liste 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 au lieu d'une liste d'entre eux. return ne vous permet jamais de l'emporter avec bonheur, il suffit de le mettre en œuvre une fois et d'arrêter.

  1. Il vient yield

Remplacez return par 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.

Si vous comparez à return qui fonctionne une fois et s’arrête, yield exécute les heures planifiées . Vous pouvez interpréter return comme return one of them et yield comme return all of them. Ceci s'appelle iterable.

  1. Une étape supplémentaire nous pouvons réécrire l'instruction yield avec return
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 le noyau de yield.

La différence entre une sortie de la liste return et la sortie de l'objet yield est la suivante:

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

En conclusion, comme métaphore pour le dire:

  • return et yield sont des jumeaux
  • list et generator sont des jumeaux
86
JawSaw

Voici quelques exemples Python montrant comment 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))

Utiliser des 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'objet 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)
83
Dustin Getz

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

En outre, notez que yield peut être utilisé dans les routines comme le double de leur utilisation dans les fonctions du générateur. Bien que ce ne soit pas la même utilisation que votre extrait de code, (yield) peut être utilisé comme expression dans une fonction. Lorsqu'un appelant envoie une valeur à la méthode à l'aide de la méthode send(), la coroutine est exécutée jusqu'à la prochaine instruction (yield).

Les générateurs et les routines sont un moyen pratique de configurer des applications de type flux de données. Je pensais qu'il valait la peine de connaître l'autre utilisation de la déclaration yield dans les fonctions.

81
johnzachary

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

Pour implémenter des itérateurs, des générateurs et des pools de threads pour une exécution simultanée, etc. sous la forme de thunks (également appelées fonctions anonymes), on utilise les messages envoyés à un objet de fermeture, qui possède un répartiteur, qui répond aux "messages".

_ { http://en.wikipedia.org/wiki/Message_passing } _

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

Il y a beaucoup de façons d'implémenter ce calcul. J'ai utilisé 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
->
77
alinsoar

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)

Sortie:

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 un développeur Python, mais il me semble que yield conserve la position de déroulement du programme et que la boucle suivante part de la position "rendement". Il semble qu’il attende à cette position et, juste avant, renvoie une valeur à l’extérieur et continue de fonctionner.

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

70
Engin OZTURK

Voici une image mentale de ce que yield fait.

J'aime penser qu'un thread possède 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 des calculs, efface la pile et retourne. Les valeurs de ses variables locales ne sont plus jamais vues.

Avec une fonction yield, lorsque son code commence à s'exécuter (c'est-à-dire après l'appel de la fonction, en renvoyant un objet générateur, dont la méthode next() est ensuite appelée), il place également ses variables locales dans la pile et les calcule pendant un certain temps. Mais ensuite, lorsqu'il atteint l'instruction yield, avant d'effacer sa partie de la pile et de la renvoyer, 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'instruction yield particulière).

Il s’agit donc d’une fonction figée à laquelle le générateur est suspendu.

Lorsque next() est appelé par la suite, il récupère les objets de la fonction dans la pile et les réanime. La fonction continue de calculer à partir de là où elle s'était arrêtée, oubliant le fait qu'elle venait de passer une éternité dans un entrepôt frigorifique.

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 déclaration yield peut être inaccessible, mais si elle est présente n'importe où, elle change la nature de ce à quoi nous sommes confrontés.

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

L'appel de yielderFunction() ne lance pas son code, mais en fait un générateur. (Peut-être que c'est une bonne idée de nommer de telles choses avec le préfixe yielder pour plus de 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_code et gi_frame sont ceux dans lesquels l'état figé est stocké. En les explorant avec dir(..), nous pouvons confirmer que notre modèle mental ci-dessus est crédible.

58
Evgeni Sergeev

Comme chaque réponse le suggère, yield est utilisé pour créer un générateur de séquence. Il est utilisé pour générer une séquence dynamiquement. Par exemple, lors de la lecture d'un fichier ligne par ligne sur un réseau, vous pouvez utiliser la fonction yield 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)

Transfert du contrôle d'exécution exécuté

Le contrôle d'exécution sera transféré de getNextLines () à la boucle for lorsque le rendement est exécuté. Ainsi, chaque fois que getNextLines () est appelée, l'exécution commence à partir du point où elle avait été suspendue 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"
49

Le rendement est un objet

Un return dans une fonction retournera une valeur unique.

Si vous voulez une fonction retourne un très grand nombre de valeurs , utilisez yield.

Plus important encore, yield est un obstacle .

comme la barrière dans la langue CUDA, il ne transférera pas le contrôle jusqu'à ce qu'il obtienne terminé.

En d’autres termes, le code sera exécuté dans votre fonction depuis le début jusqu’à atteindre la variable yield. Ensuite, il retournera la première valeur de la boucle.

Ensuite, chaque autre appel exécutera la boucle que vous avez écrite dans la fonction une autre fois, renvoyant la valeur suivante jusqu'à ce qu'il ne reste plus aucune valeur à renvoyer.

43
Kaleem Ullah

En résumé, l'instruction yield transforme votre fonction en une fabrique qui produit un objet spécial appelé generator qui enveloppe le corps de votre fonction d'origine. Lorsque la variable generator est itérée, la fonction est exécutée jusqu'à atteindre la variable yield suivante, puis l'exécution est suspendue et la valeur passée à yield est évaluée. 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

simplement des sorties

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 «donner» le résultat suivant du calcul, ce qui permet de calculer 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 fonction range 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 c'est inefficace parce que

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

Heureusement, Guido et son équipe ont eu la générosité de développer des générateurs afin que nous puissions le faire.

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

for i in myRangeSmart(10):
    print i

Désormais, à chaque itération, une fonction du générateur appelée 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() exécute jusqu’à la déclaration de rendement et cède 'n', lors du prochain appel, il exécutera la déclaration d’incrémentation, retournera à la requête 'while', l’évaluera et, si elle est vraie, s’arrêtera et céder 'n' à nouveau, cela continuera jusqu'à ce que la condition while retourne false et que le générateur passe à la fin de la fonction.

42
redbandit

Beaucoup de gens utilisent return plutôt que yield, mais dans certains cas, yield peut être plus efficace et plus facile à utiliser.

Voici un exemple pour lequel yield est 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 yield utilise trois lignes au lieu de cinq et a une variable de moins à prendre en compte.

Ceci est le résultat du code:

 Output

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

Un exemple concret serait par exemple de lire un fichier ligne par ligne ou de créer un générateur.

40
Tom Fuller

yield est comme un élément de retour pour une fonction. La différence est que l'élément yield 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 «produites» en une seule en appelant list(generator()).

35
Theoremiser

Le mot clé yield recueille simplement les résultats renvoyés. Pensez à yield comme return +=

35
Bahtiyar Özdere

Voici une approche simple basée sur yield, pour calculer la série de fibonacci, a expliqué:

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

Lorsque vous entrez ceci dans votre REPL et que vous essayez ensuite de l'appeler, vous obtenez un résultat mystificateur:

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

En effet, la présence de yield signalant à 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 réalisé directement à l'aide de la fonction intégrée next ou indirectement en l'insérant dans une construction qui consomme des valeurs. 

A l'aide de la fonction next() intégrée, vous appelez directement le .next/__next__, ce qui oblige 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 boucle for, à un initialiseur list, à un initialiseur Tuple 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 ( et ça 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 initialiseur Tuple

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

Un générateur diffère d'une fonction en ce sens qu'il est paresseux. Pour ce faire, il maintient son état local et vous permet de le reprendre à tout moment. 

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

f = fib()

Python compile la fonction, rencontre le mot clé yield et renvoie simplement un objet générateur à votre retour. Pas très utile semble-t-il. 

Lorsque vous lui demandez ensuite de générer la première valeur, directement ou indirectement, il exécute toutes les instructions trouvées jusqu'à ce qu'il rencontre une yield. Il renvoie ensuite la valeur que vous avez fournie à yield et se met en pause. Pour un exemple qui illustre mieux cela, utilisons quelques appels print (remplacez par print "text" si 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 qu'il génère une valeur. Utilisez next et 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 cités sont ce qui est imprimé. Le résultat cité est ce qui est retourné de yield. Appelez next encore une fois 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 a été mis en pause à yield value et reprend à partir de là. Le message suivant est imprimé et la recherche de l'instruction yield pour y suspendre est exécutée à nouveau (en raison de la boucle while).

32

Un exemple simple de ce que cela explique facilement: 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
29
Gavriel Cohen

Encore un autre TL; DR

Iterator on list: next() renvoie l'élément suivant de la liste

Le générateur Iterator: next() calculera le prochain élément à la volée (code d'exécution)

Vous pouvez voir le rendement/générateur comme un moyen d'exécuter manuellement le flux control de l'extérieur (comme avec la boucle continue une étape), en appelant next, quelle que soit la complexité du flux.

Note: Le générateur est ET NON une fonction normale. Il se souvient de l'état précédent comme des variables locales (pile). Voir d'autres réponses ou articles pour une explication détaillée. Le générateur ne peut être itéré qu'une seule fois. Vous pourriez vous passer de yield, mais ce ne serait pas aussi agréable, donc cela peut être considéré comme du sucre «très gentil».

28
Christophe Roussy

rendement est similaire à retour. La différence est: 

yield rend une fonction itérable (dans l'exemple suivant, la fonction primes(n = 1) devient itérable).
Cela signifie essentiellement que la prochaine fois que la fonction sera appelée, elle continuera là où elle est partie (après la ligne yield expression).

def isprime(n):
    if n == 1:
        return False
    for x in range(2, n):
        if n % x == 0:
            return False
    else:
        return True

def primes(n = 1):
   while(True):
       if isprime(n): yield n
       n += 1 

for n in primes():
    if n > 100: break
    print(n)

Dans l'exemple ci-dessus, si isprime(n) est true, le nombre premier sera renvoyé. A la prochaine itération, cela continuera à partir de la ligne suivante 

n += 1  
24
blueray

Toutes les réponses ici sont super; mais un seul d'entre eux (le plus voté) concerne comment votre code fonctionne. D'autres concernent générateurs en général et leur fonctionnement.

Je ne vais donc pas répéter ce que sont les générateurs ou les rendements; Je pense que ceux-ci sont couverts par d'excellentes réponses existantes. Cependant, après avoir passé quelques heures à essayer de comprendre un code similaire au vôtre, je vais vous expliquer comment cela fonctionne.

Votre code traverse une arborescence binaire. Prenons cet arbre par exemple:

    5
   / \
  3   6
 / \   \
1   4   8

Et une autre implémentation plus simple d'une traversée d'arborescence de recherche binaire:

class Node(object):
..
def __iter__(self):
    if self.has_left_child():
        for child in self.left:
            yield child

    yield self.val

    if self.has_right_child():
        for child in self.right:
            yield child

Le code d'exécution est sur l'objet Tree, qui implémente __iter__ comme ceci:

def __iter__(self):

    class EmptyIter():
        def next(self):
            raise StopIteration

    if self.root:
        return self.root.__iter__()
    return EmptyIter()

L'instruction while candidates peut être remplacée par for element in tree; Python traduit cela en

it = iter(TreeObj)  # returns iter(self.root) which calls self.root.__iter__()
for element in it: 
    .. process element .. 

Étant donné que la fonction Node.__iter__ est un générateur, le code à l'intérieur de celle-ci est exécuté par itération. Donc, l'exécution ressemblerait à ceci:

  1. l'élément racine est premier; vérifie s'il a laissé des enfants et que for les répète (appelons-le it1 car c'est le premier objet itérateur)
  2. il a un enfant donc la for est exécutée. Le for child in self.left crée un nouvel itérateur à partir de self.left, qui est un objet Node lui-même (it2)
  3. Même logique que 2, et une nouvelle iterator est créée (it3)
  4. Maintenant nous avons atteint l'extrémité gauche de l'arbre. it3 n'a plus d'enfants, il continue et yield self.value
  5. Lors du prochain appel à next(it3), il lève StopIteration et existe car il n’a pas d’enfants corrects (il atteint la fin de la fonction sans rien céder)
  6. it1 et it2 sont toujours actifs - ils ne sont pas épuisés et appeler next(it2) produirait des valeurs et non une augmentation StopIteration
  7. Nous sommes maintenant de retour dans le contexte it2 et appelons next(it2) qui continue là où il s’est arrêté: juste après l’instruction yield child. Puisqu'il n'a plus d'enfants, il continue et donne son self.val.

Le problème ici est que chaque itération crée des sous-itérateurs pour parcourir l’arbre et conserve l’état de l’itérateur actuel. Une fois qu'il a atteint la fin, il retourne dans la pile et les valeurs sont renvoyées dans le bon ordre (la plus petite valeur en premier).

Votre exemple de code a fait quelque chose de similaire dans une technique différente: il a renseigné une liste de un élément pour chaque enfant, puis à la prochaine itération, il apparaît et exécute le code de fonction sur l'objet actuel (d'où la self).

J'espère que cela a contribué un peu à ce sujet légendaire. J'ai passé plusieurs bonnes heures à dessiner ce processus pour le comprendre.

10
Chen A.

En bref, l'utilisation de yield est similaire au mot clé return, sauf qu'elle renvoie un générateur .
Un objet generator ne parcourt que une fois.

rendement présente deux avantages: 

  1. Vous n'avez pas besoin de lire ces valeurs deux fois; 
  2. Vous pouvez obtenir de nombreux nœuds enfants sans les mettre tous en mémoire.
8
123

En Python, generators (un type spécial de iterators) est utilisé pour générer une série de valeurs et le mot clé yield est identique au mot clé return des fonctions du générateur. 

L’autre chose fascinante que le mot clé yield fait est d’enregistrer la state d’une fonction génératrice

Ainsi, nous pouvons définir une valeur number à une valeur différente chaque fois que la valeur generator donne. 

Voici un exemple:

def getPrimes(number):
    while True:
        if isPrime(number):
            number = yield number     # a miracle occurs here
        number += 1

def printSuccessivePrimes(iterations, base=10):
primeGenerator = getPrimes(base)
primeGenerator.send(None)
for power in range(iterations):
    print(primeGenerator.send(base ** power))
7
ARGeo

Rendement

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

En bref , vous pouvez voir que la boucle ne s'arrête pas et continue de fonctionner même après l'envoi de l'objet ou de la variable (contrairement à return où la boucle s'arrête après l'exécution).

6
Gavriel Cohen

Une analogie pourrait aider à saisir l'idée ici:

Imaginez que vous ayez créé une machine étonnante capable de générer des milliers et des milliers d'ampoules par jour. La machine génère ces ampoules dans des boîtes avec un numéro de série unique. Vous ne disposez pas de suffisamment d'espace pour stocker toutes ces ampoules en même temps (par exemple, vous ne pouvez pas suivre la vitesse de la machine en raison d'une limitation de stockage), vous souhaitez donc ajuster cette machine pour générer des ampoules à la demande.

Les générateurs Python ne diffèrent pas beaucoup de ce concept.

Imaginez que vous ayez une fonction x qui génère des numéros de série uniques pour les boîtes. De toute évidence, vous pouvez avoir un très grand nombre de codes à barres de ce type générés par la fonction. Une option plus sage et plus efficace consiste à générer ces numéros de série à la demande.

Code de la machine:

def barcode_generator():
    serial_number = 10000  # Initial barcode
    while True:
        yield serial_number
        serial_number += 1


barcode = barcode_generator()
while True:
    number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
    barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
    print(barcodes)

    # function_to_create_the_next_batch_of_lightbulbs(barcodes)

    produce_more = input("Produce more? [Y/n]: ")
    if produce_more == "n":
        break

Comme vous pouvez le constater, une "fonction" autonome permet de générer chaque fois le numéro de série unique suivant. Cette fonction retourne un générateur! Comme vous pouvez le constater, nous n’appelons pas la fonction à chaque fois qu’il nous faut un nouveau numéro de série, mais nous utilisons next() étant donné que le générateur génère le prochain numéro de série.

Sortie:

How many lightbulbs to generate? 5
[10000, 10001, 10002, 10003, 10004]
Produce more? [Y/n]: y
How many lightbulbs to generate? 6
[10005, 10006, 10007, 10008, 10009, 10010]
Produce more? [Y/n]: y
How many lightbulbs to generate? 7
[10011, 10012, 10013, 10014, 10015, 10016, 10017]
Produce more? [Y/n]: n
6
Rafael

yield Est un type de générateur pouvant être utilisé en python.

voici un lien pour voir ce que Yield fait vraiment, également en génération . Moteurs générateurs & Yield - Python Central (PC)

De plus, yield fonctionne comme return, mais d'une manière différente de return. Même s'il existe un lien qui explique yield davantage, si vous ne comprenez pas l'autre. Améliorez vos compétences de rendement - jeffknupp

3

En termes simples, «rendement» est similaire à «renvoyer» une valeur, mais cela fonctionne avec Generator.

1
user3701435

En simple rendement, retourne l'objet générateur au lieu de valeurs. 

Ci-dessous, un exemple simple aidera!

def sim_generator():
    for i in range(3):
        yield(i)

obj = sim_generator()
next(obj) # another way is obj.__next__()
next(obj)
next(obj)

le code ci-dessus renvoie 0, 1, 2

ou même court

for val in sim_generator():
    print(val)

retourne 0, 1, 2

J'espère que cela t'aides

1
Vivek Ananthan

Une fonction de générateur simple

def my_gen():
    n = 1
    print('This is printed first')
    # Generator function contains yield statements
    yield n

    n += 1
    print('This is printed second')
    yield n

    n += 1
    print('This is printed at last')
    yield n

l'instruction de rendement met la fonction en pause, en enregistrant tous ses états, puis se poursuit lors d'appels successifs.

https://www.programiz.com/python-programming/generator

0
Savai Maheshwari

yield donne quelque chose. C'est comme si quelqu'un vous demandait de faire 5 petits gâteaux. Si vous avez terminé avec au moins un gâteau, vous pouvez le leur donner à manger pendant que vous faites d'autres gâteaux.

In [4]: def make_cake(numbers):
   ...:     for i in range(numbers):
   ...:         yield 'Cake {}'.format(i)
   ...:

In [5]: factory = make_cake(5)

Ici factory s'appelle générateur, ce qui vous fait des gâteaux. Si vous appelez make_function, vous obtenez un générateur au lieu d’exécuter cette fonction. C'est parce que lorsque le mot clé yield est présent dans une fonction, il devient un générateur.

In [7]: next(factory)
Out[7]: 'Cake 0'

In [8]: next(factory)
Out[8]: 'Cake 1'

In [9]: next(factory)
Out[9]: 'Cake 2'

In [10]: next(factory)
Out[10]: 'Cake 3'

In [11]: next(factory)
Out[11]: 'Cake 4'

Ils ont consommé tous les gâteaux, mais ils en redemandent un.

In [12]: next(factory)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-12-0f5c45da9774> in <module>
----> 1 next(factory)

StopIteration:

et on leur dit d'arrêter de demander plus. Donc, une fois que vous avez consommé un générateur, vous en avez fini. Vous avez besoin d'appeler à nouveau make_cake si vous voulez plus de gâteaux. C'est comme passer une autre commande pour des gâteaux.

In [13]: factory = make_cake(3)

In [14]: for cake in factory:
    ...:     print(cake)
    ...:
Cake 0
Cake 1
Cake 2

Vous pouvez également utiliser pour la boucle avec un générateur comme celui ci-dessus.

Un autre exemple: disons que vous voulez un mot de passe aléatoire chaque fois que vous le demandez.

In [22]: import random

In [23]: import string

In [24]: def random_password_generator():
    ...:     while True:
    ...:         yield ''.join([random.choice(string.ascii_letters) for _ in range(8)])
    ...:

In [25]: rpg = random_password_generator()

In [26]: for i in range(3):
    ...:     print(next(rpg))
    ...:
FXpUBhhH
DdUDHoHn
dvtebEqG

In [27]: next(rpg)
Out[27]: 'mJbYRMNo'

Ici rpg est un générateur qui peut générer un nombre infini de mots de passe aléatoires. Nous pouvons donc aussi dire que les générateurs sont utiles lorsque nous ne connaissons pas la longueur de la séquence, contrairement à la liste qui contient un nombre fini d'éléments.

0
thavan