python itérateur iterable - À quoi sert le mot clé «rendement»?




15 Answers

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 .

iteration yield

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 .




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. :)




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.




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




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.




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.




Il y a un autre yieldusage et signification (depuis Python 3.3):

yield from <expr>

A partir 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 et cette valeur est mise à la disposition du générateur qui délègue.

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

De plus, this introduira (depuis Python 3.5):

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

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




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



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



Voici une image mentale de ce qui yieldfait.

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 yieldfonction, lorsque son code commence à s'exécuter (c'est-à-dire après que la fonction a été appelée, renvoyant un objet générateur, dont la next()méthode est ensuite appelée), il place également ses variables locales dans la pile et les calcule pendant un certain temps. Mais ensuite, lorsqu'il atteint l' yieldinstruction, avant de vider 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 la yielddéclaration particulière ).

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

Quand next()est appelé ultérieurement, 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 yielddéclaration peut être inaccessible, mais si elle est présente n'importe où, cela change la nature de ce à quoi nous sommes confrontés.

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

L'appel 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 yielderpréfixe pour la lisibilité.)

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

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




Le rendement est un objet

Un returndans une fonction retournera une valeur unique.

Si vous voulez qu'une fonction retourne un grand nombre de valeurs , utilisez yield.

Plus important encore, yieldest un obstacle .

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

Autrement dit, le code de votre fonction sera exécuté du début à la fin 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 n'y ait plus aucune valeur à renvoyer.




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




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

Voici un exemple qui yieldconvient vraiment pour:

retour (en fonction)

import random

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

rendement (en fonction)

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

Fonctions d'appel

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

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

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

Voici le résultat du code:

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

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




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




Related