itérateur - python yield return list




À quoi sert le mot clé «rendement»? (20)

Que fait le mot-clé yield en Python?

Plan de réponse / résumé

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

Générateurs:

yield n'est légal que dans une définition de fonction, et l'inclusion du yield dans une définition de fonction fait en sorte qu'elle retourne un générateur.

L'idée des générateurs vient d'autres langues (voir référence 1) avec des implémentations variées. Dans Python's Generators, l'exécution du code est frozen au point du rendement. Lorsque le générateur est appelé (les méthodes sont décrites ci-dessous), l'exécution reprend et se fige au prochain rendement.

yield fournit un moyen simple d' implémenter le protocole itérateur , défini par les deux méthodes suivantes: __iter__ et next (Python 2) ou __next__ (Python 3). Ces deux méthodes font d'un objet un itérateur que vous pouvez vérifier avec la classe de base abstraite Iterator partir du module collections .

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

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

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

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

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

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

On peut générer 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 au suivant: à partir de Python 3.3 (et non disponible dans Python 2), vous pouvez utiliser le yield from :

def func(an_iterable):
    yield from an_iterable

Cependant, le yield from permet également la délégation à des sous-générateurs, ce qui sera expliqué dans la section suivante sur la délégation coopérative avec des sous-routines.

Coroutines:

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

Voici un exemple, prenez note de la variable received , qui pointera sur 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)

Premièrement, nous devons mettre le générateur en file d'attente avec la fonction intégrée, next . Il appellera la méthode next ou __next__ appropriée, selon la version de Python que vous utilisez:

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

Et maintenant, nous pouvons envoyer des données au générateur. ( Envoi None est identique à l'appel next .):

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

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

Maintenant, rappelons que le yield from est disponible dans Python 3. Cela nous permet de déléguer des routines à 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, qui peut être utilisé par un générateur comme ci-dessus:

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

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

Autres méthodes: fermer et jeter

La méthode close soulève GeneratorExit au moment où l'exécution de la fonction a été gelée. Ceci sera également appelé par __del__ pour que vous puissiez mettre n'importe quel code de nettoyage à l'endroit où vous gérez GeneratorExit :

>>> my_account.close()

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

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

Conclusion

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

Que fait le mot-clé yield en Python?

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

Appendice:

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

  • Il est confus sur ce qui rend un itérable , juste en utilisant une liste comme exemple. Voir mes références ci-dessus, mais en résumé: un itérable a une méthode __iter__ renvoyant un itérateur . Un itérateur fournit une .next (Python 2 ou .__next__ (Python 3), qui est appelée implicitement par for boucles for jusqu'à ce qu'elle lève StopIteration , et une fois que cela se produit, il continuera à le faire.
  • Il utilise ensuite une expression de générateur pour décrire ce qu'est un générateur. Comme un générateur est simplement un moyen pratique de créer un itérateur , cela ne fait que brouiller les pistes et nous n’en sommes pas encore au stade du yield .
  • En contrôlant l'épuisement d'un générateur, il appelle la méthode .next , alors qu'il doit 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 ce que le yield fait du tout.
  • Aucune discussion sur les méthodes utilisées ne fournit une information sur le yield from nouvelles fonctionnalités yield from Python 3. La réponse top / acceptée est une réponse très incomplète.

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

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

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

Étant donné que le rendement est une expression, il a été présenté par certains comme étant intéressant à utiliser comme expression de compréhension ou comme générateur d’expression - en dépit du fait de ne pas citer de cas d’utilisation particulièrement intéressant.

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

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

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

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

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

Je conviens que c’est un endroit judicieux pour nous, car tout code reposant sur le comportement actuel est vraiment trop intelligent pour être maintenable.

Pour ce faire, nous voudrons probablement:

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

Vive Nick.

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

En outre, un problème en suspens (10544) semble indiquer que cette idée n’est jamais une bonne idée (PyPy, une implémentation Python écrite en Python, génère déjà des avertissements concernant la syntaxe.)

En bout de ligne, jusqu'à ce que les développeurs de CPython nous disent le contraire: Ne mettez pas yielddans une expression ou une compréhension de générateur.

La returndéclaration dans un générateur

En Python 2 :

Dans une fonction génératrice, l' returninstruction n'est pas autorisée à inclure un expression_list. Dans ce contexte, un nu returnindique que le générateur est terminé et qu'il StopIterationsera surélevé.

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

En Python 3 :

Dans une fonction de générateur, la returndéclaration indique que le générateur est terminé et qu'il StopIterationsera surélevé. La valeur renvoyée (le cas échéant) est utilisée comme argument à construire StopIterationet devient l' StopIteration.valueattribut.

Notes de bas de page

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

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

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

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

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

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

Et voici l'appelant:

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

Que se passe-t-il lorsque la méthode _get_child_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. Ceci est le lien vers la source complète: Module mspace .


Raccourci vers le yield Grokking

Lorsque vous voyez une fonction avec yield déclarations de yield , appliquez cette astuce simple pour comprendre ce qui va se passer:

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

Cette astuce peut vous donner une idée de la logique de la fonction, mais ce qui se produit réellement avec le 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 itérateur - lorsque vous écrivez

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

Python effectue les deux étapes suivantes:

  1. Obtient un itérateur pour mylist :

    Appel iter(mylist) -> Ceci retourne un objet avec une méthode next() (ou __next__() dans 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 StopIteration à 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.

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

Ici, mylist est un itératif car il implémente le protocole itérateur. Dans une classe définie par l'utilisateur, vous pouvez implémenter la __iter__() pour rendre les instances de votre classe itérables. Cette méthode devrait 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 que __iter__() retourne. Cela fonctionnera pour des cas simples, mais pas si vous voulez 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 lors de l'appel de 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.

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Au lieu des instructions de yield , si vous aviez trois instructions de 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 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 boucler sur l'objet générateur, la fonction reprend son état suspendu à la ligne suivante après le yield elle était précédemment renvoyée, exécute la ligne de code suivante, dans ce cas une instruction de yield , et le 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 StopIteration .

Ainsi, l'objet générateur ressemble à un adaptateur: à une extrémité, il présente le protocole d'itérateur, en exposant les __iter__() et next() pour que la boucle for reste heureuse. Toutefois, à 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 cela peut rendre l'utilisation de la mémoire inefficace 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__() dans Python 3). Selon la logique, le code de la méthode next() peut avoir l’air très complexe et être sujet à des bogues. Ici, les générateurs offrent une solution propre et facile.


Pense-y de cette façon:

Un itérateur n'est qu'un terme à la sonorité sophistiquée désignant un objet ayant 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 trouble-t-il plus? :)

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


Pour comprendre ce que fait le yield , vous devez comprendre ce que sont les générateurs . Et avant les générateurs viennent 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 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 seule fois . Les générateurs ne stockent pas toutes les valeurs en mémoire, ils les génèrent à la volée :

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

C'est exactement pareil sauf que vous avez utilisé () au lieu de [] . MAIS, vous ne pouvez pas effectuer une seconde exécution for i in mygenerator car les générateurs ne peuvent être utilisés qu’une seule fois: ils calculent 0, puis l’oublient et calculent 1, et terminent le calcul de 4, un par un.

rendement

yield est un mot clé qui est utilisé comme return , sauf que la fonction retournera un générateur.

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

Ici, c’est un exemple inutile, mais c’est bien pratique quand vous savez que votre fonction renverra un énorme ensemble de valeurs qu’il n’aura besoin que de lire une fois.

Pour maîtriser le 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 sera exécuté chaque fois que 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 exécutera le code dans votre fonction depuis le début jusqu'à ce qu'il atteigne un yield , puis 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 n'atteint plus le yield . Cela peut être dû au fait que la boucle est terminée ou que vous ne satisfaisez plus un "if/else" .

Votre code expliqué

Générateur:

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

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

    # If there is still a child of the node object on its left
    # AND if 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 un moyen concis de parcourir toutes ces données imbriquées, même si c'est un peu dangereux, car vous pouvez vous retrouver avec une boucle infinie. Dans ce cas, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) épuise toutes les valeurs du générateur, mais continue de créer de nouveaux objets générateur qui produiront des valeurs différentes des précédentes, car il n'est pas appliqué de la même manière. 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 iterables, donc il fonctionnera avec des chaînes, des listes, des n-uplets et des générateurs! C'est ce que nous appelons la frappe de canard et c'est l'une des raisons pour lesquelles Python est si cool. Mais ceci est une autre histoire, pour une autre question ...

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

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

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

Remarque: Pour Python 3, utilisez print(corner_street_atm.__next__()) 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 deux générateurs? Grouper les valeurs dans une liste imbriquée avec un one-liner? Map / Zip sans créer une autre liste?

Ensuite, import itertools simplement 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émentant la __iter__() ) et des itérateurs (implémentant la __next__() ). Les itérables sont tous les objets dont vous pouvez obtenir un itérateur. Les itérateurs sont des objets qui vous permettent d'itérer sur des itérables.

Cet article traite plus en détail du fonctionnement for boucles for .


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


yieldest juste 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' yieldinstruction. Contrairement au retour, le cadre de pile n'est pas nettoyé lorsqu’un rendement se produit. Toutefois, le contrôle est transféré à l’appelant. Son état est rétabli lors de la prochaine exécution de la fonction.

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

list.extendappelle 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.


TL; DR

Au lieu de cela:

def squares_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 squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Chaque fois que vous vous trouvez en train de construire une liste à partir de zéro, yieldchaque morceau à la place.

Ce fut mon premier moment "aha" avec rendement.

yield est une façon sucrée de dire

construire une série de choses

Même comportement:

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

Comportement différent:

Le rendement est en un seul passage : vous ne pouvez parcourir qu'une seule fois. Quand une fonction a un rendement, on l’appelle une fonction génératrice . Et un iterator est ce qu'il retourne. C'est révélateur. Nous perdons la commodité d'un conteneur mais gagnons en puissance une série arbitrairement longue.

Le rendement est paresseux , cela retarde les calculs. Une fonction contenant un rendement ne s’exécute pas du tout lorsque vous l’appelez. L'objet itérateur qu'il renvoie utilise la magic pour conserver le contexte interne de la fonction. Chaque fois que vous appelez next()l'itérateur (cela se produit dans une boucle for), l'exécution avance petit à petit au prochain rendement. ( returnsoulève StopIterationet termine la série.)

Le rendement est polyvalent . Il peut faire des boucles infinies:

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

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

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

Choix génial du mot yieldcar 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.


Encore un autre TL; DR

Iterator on list : next()renvoie le prochain élément de la liste

Générateur Iterator : next()calculera le prochain élément à la volée (exécuter du code)

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

Remarque : le générateur n'est PAS 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 en passer yield, mais ce ne serait pas aussi agréable. On peut donc parler de sucre «très agréable».


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 une valeur 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, ceci 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 dans 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.


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

Je suppose que vous avez appris la returndéclaration.

Par analogie, returnet yieldsont des jumeaux. returnsignifie 'revenir et arrêter' alors que 'rendement' signifie 'retourner, mais continuer'

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

Exécuter:

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

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

  1. Il vient yield

Remplacer returnpar yield:

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

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

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

Maintenant, vous gagnez pour obtenir tous les chiffres.

En comparant à returnqui fonctionne une fois et s'arrête, les yieldtemps que vous avez planifiés. Vous pouvez interpréter returncomme return one of themet yieldcomme return all of them. Ceci s'appelle iterable.

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

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

C'est le coeur à propos yield.

La différence entre une returnsortie de liste et la yieldsortie d' objet 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 "la yieldsortie de l'objet ". Ainsi, il a un nouvel generatorobjet de nom comme indiqué dans Out[11]: <generator object num_list at 0x10327c990>.

En conclusion, comme métaphore pour le dire:

  • returnet yieldsont jumeaux
  • listet generatorsont jumeaux

À mon avis, il n’existe pas encore un 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' yieldinstruction en Python renvoie un générateur. Un générateur en Python est une fonction qui renvoie des continuations (et plus précisément un type de coroutine, mais les continuations représentent le mécanisme plus général permettant de 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 à exécuter, etc. sont enregistrées. Ensuite, à un moment ultérieur du programme, la suite peut être appelée, de telle sorte que les variables du programme sont réinitialisées à cet état et que les opérations sauvegardées sont effectuées.

Les continuations, sous cette forme plus générale, peuvent être mises en œuvre de deux manières. De la même call/ccmanière, la pile du programme est littéralement sauvegardée puis, lorsque la 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 argument (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. Voici un exemple très simple de style de passage de continuation:

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 (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 perdre 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 sont généralement capables de sauvegarder l'état d'un calcul (c'est-à-dire la pile d'appels du programme), les générateurs ne peuvent sauvegarder que l'état d'itération 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'iterable 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. C’est plus facile à mettre en œuvre par le concepteur de langage et plus facile à 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 qu'il yieldest 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 de pseudo-code, mais pas de code), la nextméthode du générateur est essentiellement la suivante:

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

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

où le yieldmot clé est en fait un sucre syntaxique pour la fonction de générateur réelle, à peu près comme:

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 en Python est plus complexe. Mais comme exercice pour comprendre ce qui se passe, essayez d’utiliser le style de passage de continuation pour implémenter des objets générateurs sans utiliser le yieldmot - clé.


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

Quand yieldest utilisé à la place d'une returnfonction python, cette fonction est transformée en quelque chose de spécial appelé generator function. Cette fonction retournera un objet de generatortype. Le yieldmot clé est un drapeau pour informer le compilateur python de traiter cette fonction de manière spécifique. Les fonctions normales se termineront dès qu'une valeur sera renvoyée. Mais avec l’aide du compilateur, la fonction de générateur peut être considérée comme pouvant être reprise. En d'autres termes, le contexte d'exécution sera restauré et l'exécution se poursuivra à partir de la dernière exécution. Jusqu'à ce que vous appeliez explicitement return, ce qui déclencherait une StopIterationexception (qui fait également partie du protocole itérateur) ou atteindrait la fin de la fonction. J'ai trouvé beaucoup de références generatormais celui- onede la functional programming perspectiveest la plus digeste.

(Maintenant, je veux parler de la raison derrière generator, et de celle iteratorbasée sur ma propre compréhension. J'espère que cela peut vous aider à comprendre la motivation essentielle de l'itérateur et du générateur. Ce concept apparaît également dans d'autres langages tels que C #.)

Si je comprends bien, lorsque nous voulons traiter un ensemble de données, nous les stockons d’abord quelque part, puis nous les traitons une à une. Mais cette approche intuitive est problématique. Si le volume de données est énorme, il est coûteux de les stocker au préalable dans leur ensemble. Donc, au lieu de stocker le datalui - même directement, pourquoi ne pas stocker metadataindirectement, c.-à-dthe logic how the data is computed .

Il existe 2 approches pour envelopper de telles métadonnées.

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

Dans les deux cas, un itérateur est créé, c’est-à-dire un objet pouvant vous fournir les données souhaitées. L'approche OO peut être un peu complexe. Quoi qu'il en soit, lequel choisir est à vous.


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

Pour vous aider à comprendre ce que yieldfait le code suivant, vous pouvez utiliser votre doigt pour suivre le cycle à travers tout code comportant un yield. Chaque fois que votre doigt frappe le bouton yield, vous devez attendre qu'un nextou plusieurs sendsoient saisis. Quand a nextest appelé, vous tracez le code jusqu'à ce que vous appuyiez sur yield… le code à droite de celui-ci yieldsoit évalué et renvoyé à l'appelant… puis vous attendez. Quand nextest rappelé, vous effectuez une autre boucle dans le code. Cependant, vous remarquerez que dans une coroutine, yieldpeut également être utilisé avec un send… qui enverra une valeur de l'appelant dans la fonction de cession. Si un sendest donné, alorsyieldreçoit la valeur envoyée et la crache à gauche… puis la trace dans le code progresse jusqu'à ce que vous appuyiez de yieldnouveau sur (en renvoyant la valeur à la fin, comme si elle nextétait appelée).

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

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

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

Vous pouvez l'utiliser dans votre code comme suit:

for line in getNextLines():
    doSomeThing(line)

Transfert de contrôle d'exécution obtenu

Le contrôle d'exécution sera transféré de getNextLines () vers la forboucle 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é mise en pause 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"

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

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

for i in simple_generator():
    print i

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 rangefonction qui produit une plage de nombres itérable, vous pouvez le faire comme ça,

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

et l'utiliser comme ça;

for i in myRangeNaive(10):
    print i

Mais 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 parcourt en fait deux fois ce tableau! :(

Heureusement, Guido et son équipe ont été assez généreux pour 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

Maintenant, à 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()s’exécute jusqu’à la déclaration de rendement et renvoie 'n', lors du prochain appel, il exécutera la déclaration d’incrémentation, retournera au 'while', l’évaluera et, si la valeur est vraie, s’arrêtera et à nouveau, cela continuera jusqu'à ce que la condition while retourne false et que le générateur passe à la fin de la fonction.


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

L'idée principale 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.


Il y a une chose supplémentaire à mentionner: une fonction qui cède n'a pas à 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.


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


Voici un exemple en langage clair. Je vais établir une correspondance entre les concepts humains de haut niveau et les concepts Python de bas niveau.

Je veux opérer sur une séquence de nombres, mais je ne veux pas 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 à l’ defintroduction de la fonction génératrice, 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 retourne 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 .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 à la fin du travail de l'objet générateur et à la création d'une StopIterationexception. La fonction du générateur n'a pas besoin de déclencher l'exception. Il est déclenché automatiquement lorsque la fonction se termine ou émet un return.

C’est ce que fait un générateur (une fonction qui contient un yield); il commence à s'exécuter, se met en pause chaque fois qu'il effectue un test yield, et lorsqu'on lui demande une .next()valeur, il continue à partir du dernier moment. 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.

Le plus célèbre utilisateur du protocole itérateur est la forcommande en Python. Donc, chaque fois que vous faites un:

for item in sequence:

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

Notez qu'une deffonction contenant un yieldmot-clé 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 plus d'informations, consultez les types d'itérateurs , la déclaration de rendement et les generators dans la documentation Python.


Voici une yieldapproche simple 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>

Cela est dû au fait yieldque vous souhaitez signaler à Python que vous souhaitez créer un générateur , c'est-à-dire un objet qui génère des valeurs à la demande.

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

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

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

Indirectement, si vous fournissez fibà une forboucle, un listinitialiseur, un tupleinitialiseur ou toute autre chose 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 qu'il retourne) :

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

De même, avec un tupleinitialiseur:

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

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

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

f = fib()

Python compile la fonction, rencontre le yieldmot clé et vous renvoie simplement un objet générateur. 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 un yield, puis il renvoie la valeur que vous avez fournie yieldet met en pause. Pour un exemple qui illustre mieux cela, utilisons quelques printappels (remplacez par print "text"if 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 un objet générateur en attente d'une commande pour qu'il génère une valeur. Utilisez nextet voyez ce qui est imprimé:

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

Les résultats non cités sont ce qui est imprimé. Le résultat cité est ce qui est retourné yield. Rappelez nextmaintenant:

>>> 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 était en pause yield valueet reprend à partir de là. Le message suivant est imprimé et la recherche de l' yieldinstruction à suspendre est exécutée à nouveau (en raison de la whileboucle).





coroutine