Dans la pratique, quelles sont les principales utilisations de la nouvelle syntaxe «yield from» dans Python 3.3?

407

J'ai du mal à enrouler mon cerveau autour de PEP 380 .

  1. Dans quelles situations le «rendement de» est-il utile?
  2. Quel est le cas d'utilisation classique?
  3. Pourquoi est-il comparé aux micro-threads?

[ mise à jour ]

Maintenant, je comprends la cause de mes difficultés. J'ai utilisé des générateurs, mais je n'ai jamais vraiment utilisé de coroutines (introduit par PEP-342 ). Malgré certaines similitudes, les générateurs et les coroutines sont fondamentalement deux concepts différents. Comprendre les coroutines (pas seulement les générateurs) est la clé pour comprendre la nouvelle syntaxe.

Les coroutines à mon humble avis sont la fonctionnalité Python la plus obscure , la plupart des livres la rendent inutile et sans intérêt.

Merci pour les bonnes réponses, mais merci tout particulièrement à agf et à son commentaire en lien avec les présentations de David Beazley . David bascule.

Paulo Scardine
la source
7
Vidéo de la présentation dabeaz.com/coroutines de David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Réponses:

572

Éliminons d'abord une chose. L'explication qui yield from géquivaut à for v in g: yield v ne commence même pas à rendre justice à yield fromtout. Car, avouons-le, si tout ce yield fromqui est fait est d'étendre la forboucle, alors cela ne justifie pas d'ajouter yield fromau langage et d'empêcher tout un tas de nouvelles fonctionnalités d'être implémentées dans Python 2.x.

Qu'est yield from- ce qu'il établit est une connexion bidirectionnelle transparente entre l'appelant et le sous-générateur :

  • La connexion est "transparente" dans le sens où elle propagera tout aussi correctement, pas seulement les éléments générés (par exemple, les exceptions sont propagées).

  • La connexion est "bidirectionnelle" dans le sens où les données peuvent être envoyées depuis et vers un générateur.

( Si nous parlions de TCP, cela yield from gpourrait signifier "déconnecter temporairement le socket de mon client et le reconnecter à cet autre socket de serveur". )

BTW, si vous n'êtes pas sûr de ce que signifie envoyer des données à un générateur , vous devez tout supprimer et lire d' abord sur les coroutines - elles sont très utiles (contrastez-les avec les sous-routines ), mais malheureusement moins connues en Python. Curious Course sur Coroutines de Dave Beazley est un excellent début. Lisez les diapositives 24 à 33 pour une introduction rapide.

Lecture des données d'un générateur en utilisant le rendement de

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Au lieu d'itérer manuellement reader(), nous pouvons le faire yield from.

def reader_wrapper(g):
    yield from g

Cela fonctionne et nous avons éliminé une ligne de code. Et l'intention est probablement un peu plus claire (ou pas). Mais rien ne change la vie.

Envoi de données à un générateur (coroutine) en utilisant le rendement de - Partie 1

Faisons maintenant quelque chose de plus intéressant. Créons une coroutine appelée writerqui accepte les données qui lui sont envoyées et écrit dans un socket, fd, etc.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Maintenant, la question est de savoir comment la fonction d'encapsuleur doit gérer l'envoi de données à l'enregistreur, de sorte que toutes les données envoyées à l'encapsuleur soient envoyées de manière transparente au writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

L'encapsuleur doit accepter les données qui lui sont envoyées (évidemment) et doit également gérer le StopIterationmoment où la boucle for est épuisée. Évidemment, faire for x in coro: yield xne suffit pas. Voici une version qui fonctionne.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Ou, nous pourrions le faire.

def writer_wrapper(coro):
    yield from coro

Cela enregistre 6 lignes de code, le rend beaucoup plus lisible et cela fonctionne. La magie!

Envoi de données à un générateur de rendement à partir de - Partie 2 - Gestion des exceptions

Rendons les choses plus compliquées. Et si notre rédacteur devait gérer les exceptions? Disons que les writerpoignées a SpamExceptionet elles s'impriment ***si elles en rencontrent une.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Et si on ne change pas writer_wrapper? Est-ce que ça marche? Essayons

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Hum, ça ne marche pas parce x = (yield)que lève juste l'exception et tout s'arrête brutalement. Faisons-le fonctionner, mais en gérant manuellement les exceptions et en les envoyant ou en les jetant dans le sous-générateur ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

Cela marche.

# Result
>>  0
>>  1
>>  2
***
>>  4

Mais ça aussi!

def writer_wrapper(coro):
    yield from coro

Le yield fromgère de manière transparente l'envoi des valeurs ou le lancement de valeurs dans le sous-générateur.

Cependant, cela ne couvre pas tous les cas d'angle. Que se passe-t-il si le générateur extérieur est fermé? Qu'en est-il du cas où le sous-générateur renvoie une valeur (oui, en Python 3.3+, les générateurs peuvent renvoyer des valeurs), comment la valeur de retour doit-elle être propagée? La yield fromgestion transparente de tous les cas d'angle est vraiment impressionnante . yield fromfonctionne comme par magie et gère tous ces cas.

Personnellement, je pense que yield fromc'est un mauvais choix de mots clés car cela ne rend pas la nature bidirectionnelle apparente. D'autres mots clés ont été proposés (comme delegatemais ont été rejetés car l'ajout d'un nouveau mot clé à la langue est beaucoup plus difficile que de combiner les mots clés existants.

En résumé, il est préférable de penser yield fromcomme transparent two way channelentre l'appelant et le sous-générateur.

Références:

  1. PEP 380 - Syntaxe de délégation à un sous-générateur (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutines via des générateurs améliorés (GvR, Eby) [v2.5, 2005-05-10]
Praveen Gollakota
la source
3
@PraveenGollakota, dans la deuxième partie de votre question, Envoi de données à un générateur (coroutine) en utilisant le rendement de - Partie 1 , que se passe-t-il si vous avez plus de coroutines pour transférer l'élément reçu? Comme un scénario de diffuseur ou d'abonné où vous fournissez plusieurs coroutines à l'encapsuleur dans votre exemple et où les éléments doivent être envoyés à tous ou à un sous-ensemble d'entre eux?
Kevin Ghaboosi
3
@PraveenGollakota, bravo pour la grande réponse. Les petits exemples me permettent d'essayer les choses en repl. Le lien vers le cours Dave Beazley était un bonus!
BiGYaN
1
faire à l' except StopIteration: passintérieur de la while True:boucle n'est pas une représentation précise de yield from coro- qui n'est pas une boucle infinie et après avoir coroété épuisé (c'est-à-dire déclenche StopIteration), writer_wrapperexécutera l'instruction suivante. Après la dernière déclaration, il se StopIteration
lèvera
1
... donc s'il est writercontenu à la for _ in range(4)place de while True, puis après l'impression, >> 3il serait également augmenté automatiquement StopIterationet cela serait géré automatiquement par yield from, puis writer_wrapperaugmenterait automatiquement le sien StopIterationet parce qu'il wrap.send(i)n'est pas dans le trybloc, il serait en fait élevé à ce stade ( c'est-à-dire que traceback ne rapportera que la ligne avec wrap.send(i), rien de l'intérieur du générateur)
Aprillion
3
En lisant " ne commence même pas à faire justice ", je sais que je suis arrivé à la bonne réponse. Merci pour la grande explication!
Hot.PxL
89

Dans quelles situations le «rendement de» est-il utile?

Chaque situation où vous avez une boucle comme celle-ci:

for x in subgenerator:
  yield x

Comme le décrit le PEP, il s'agit d'une tentative plutôt naïve d'utiliser le sous-générateur, il manque plusieurs aspects, en particulier la bonne gestion des mécanismes .throw()/ .send()/ .close()introduits par le PEP 342 . Pour faire cela correctement, un code assez compliqué est nécessaire.

Quel est le cas d'utilisation classique?

Considérez que vous souhaitez extraire des informations d'une structure de données récursive. Disons que nous voulons obtenir tous les nœuds feuilles dans un arbre:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Plus important encore est le fait que jusqu'à la yield from, il n'y avait pas de méthode simple pour refactoriser le code du générateur. Supposons que vous ayez un générateur (insensé) comme celui-ci:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Vous décidez maintenant de factoriser ces boucles dans des générateurs distincts. Sans yield from, c'est moche, au point où vous réfléchirez à deux fois si vous voulez vraiment le faire. Avec yield from, c'est vraiment agréable de regarder:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Pourquoi est-il comparé aux micro-threads?

Je pense que ce dont parle cette section du PEP, c'est que chaque générateur a son propre contexte d'exécution isolé. Associé au fait que l'exécution est commutée entre le générateur-itérateur et l'appelant à l'aide yieldet __next__()respectivement, cela est similaire aux threads, où le système d'exploitation commute de temps en temps le thread d'exécution, ainsi que le contexte d'exécution (pile, registres, ...).

L'effet est également comparable: le générateur-itérateur et l'appelant progressent dans leur état d'exécution en même temps, leurs exécutions sont entrelacées. Par exemple, si le générateur effectue une sorte de calcul et que l'appelant imprime les résultats, vous verrez les résultats dès qu'ils seront disponibles. Il s'agit d'une forme de concurrence.

Cette analogie n'est pas quelque chose de spécifique yield from, cependant - c'est plutôt une propriété générale des générateurs en Python.

Niklas B.
la source
La refactorisation des générateurs est douloureuse aujourd'hui.
Josh Lee
1
J'ai tendance à utiliser beaucoup itertools pour refactoriser les générateurs (des trucs comme itertools.chain), ce n'est pas si grave. J'aime le rendement, mais je ne vois toujours pas à quel point il est révolutionnaire. C'est probablement le cas, car Guido en est complètement fou, mais je dois manquer la vue d'ensemble. Je suppose que c'est génial pour send () car c'est difficile à refactoriser, mais je ne l'utilise pas souvent.
e-satis
Je suppose que ce get_list_values_as_xxxsont de simples générateurs avec une seule ligne for x in input_param: yield int(x)et les deux autres respectivement avec stretfloat
madtyn
@NiklasB. re "extraire des informations d'une structure de données récursive." J'entre juste dans Py pour les données. Pourriez-vous essayer ce Q ?
alancalvitti
33

Chaque fois que vous invoquez un générateur de « pompe » dans un générateur , vous avez besoin d' une pour re yieldles valeurs suivantes : for v in inner_generator: yield v. Comme le PEP le souligne, il existe des complexités subtiles à cela que la plupart des gens ignorent. Un contrôle de flux non local comme throw()un exemple est donné dans le PEP. La nouvelle syntaxe yield from inner_generatorest utilisée partout où vous auriez écrit la forboucle explicite auparavant. Ce n'est pas seulement du sucre syntaxique, cependant: il gère tous les cas d'angle qui sont ignorés par la forboucle. Être «sucré» encourage les gens à l'utiliser et à adopter ainsi les bons comportements.

Ce message dans le fil de discussion parle de ces complexités:

Avec les fonctionnalités de générateur supplémentaires introduites par PEP 342, ce n'est plus le cas: comme décrit dans le PEP de Greg, l'itération simple ne prend pas correctement en charge send () et throw (). La gymnastique nécessaire pour supporter send () et throw () n'est en fait pas si complexe lorsque vous les décomposez, mais elles ne sont pas non plus triviales.

Je ne peux pas parler de comparaison avec des micro-threads, si ce n'est d'observer que les générateurs sont une sorte de paralellisme. Vous pouvez considérer le générateur suspendu comme un thread qui envoie des valeurs via yieldun thread consommateur. L'implémentation réelle peut ne rien ressembler à cela (et l'implémentation réelle est évidemment d'un grand intérêt pour les développeurs Python) mais cela ne concerne pas les utilisateurs.

La nouvelle yield fromsyntaxe n'ajoute aucune capacité supplémentaire au langage en termes de thread, elle facilite simplement l'utilisation correcte des fonctionnalités existantes. Ou plus précisément, il permet au consommateur novice d'un générateur interne complexe écrit par un expert de passer à travers ce générateur sans casser aucune de ses fonctionnalités complexes.

Ben Jackson
la source
23

Un court exemple vous aidera à comprendre l'un des yield fromcas d'utilisation: obtenir la valeur d'un autre générateur

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))
araignée
la source
2
Je voulais juste suggérer que l'impression à la fin serait un peu plus agréable sans la conversion en liste -print(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

yield from enchaîne essentiellement les itérateurs de manière efficace:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Comme vous pouvez le voir, il supprime une boucle Python pure. C'est à peu près tout ce qu'il fait, mais le chaînage des itérateurs est un modèle assez courant en Python.

Les threads sont essentiellement une fonctionnalité qui vous permet de sauter des fonctions à des points complètement aléatoires et de revenir à l'état d'une autre fonction. Le superviseur de thread fait cela très souvent, donc le programme semble exécuter toutes ces fonctions en même temps. Le problème est que les points sont aléatoires, vous devez donc utiliser le verrouillage pour empêcher le superviseur d'arrêter la fonction à un point problématique.

Les générateurs sont assez similaires aux threads dans ce sens: ils vous permettent de spécifier des points spécifiques (chaque fois qu'ils yield) où vous pouvez sauter. Lorsqu'ils sont utilisés de cette façon, les générateurs sont appelés coroutines.

Lisez cet excellent tutoriel sur les coroutines en Python pour plus de détails

Jochen Ritzel
la source
10
Cette réponse est trompeuse car elle élude la caractéristique saillante du "rendement de", comme mentionné ci-dessus: support send () et throw ().
Justin W
2
@Justin W: Je suppose que tout ce que vous avez lu est trompeur, car vous n'avez pas compris les fonctionnalités qui throw()/send()/close()doivent évidemment être correctement implémentées car elles sont censées simplifier le code. De telles banalités n'ont rien à voir avec l'usage. yieldyield from
Jochen Ritzel
5
Contestez-vous la réponse de Ben Jackson ci-dessus? Ma lecture de votre réponse est que c'est essentiellement du sucre syntaxique qui suit la transformation de code que vous avez fournie. La réponse de Ben Jackson réfute spécifiquement cette affirmation.
Justin W
@JochenRitzel Vous n'avez jamais besoin d'écrire votre propre chainfonction car elle itertools.chainexiste déjà. Utilisez yield from itertools.chain(*iters).
Acumenus
5

En utilisation appliquée pour la coroutine d'E / S asynchrone , yield froma un comportement similaire awaità celui d'une fonction de coroutine . Les deux sont utilisés pour suspendre l'exécution de la coroutine.

Pour Asyncio, s'il n'est pas nécessaire de prendre en charge une ancienne version de Python (ie> 3.5), async def/ awaitest la syntaxe recommandée pour définir une coroutine. Ainsi yield fromn'est plus nécessaire dans une coroutine.

Mais en général en dehors de asyncio, yield from <sub-generator>a encore un autre usage dans l'itération du sous-générateur comme mentionné dans la réponse précédente.

Yeo
la source
1

Ce code définit une fonction fixed_sum_digitsrenvoyant un générateur énumérant les six chiffres tels que la somme des chiffres est 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Essayez de l'écrire sans yield from. Si vous trouvez un moyen efficace de le faire, faites-le moi savoir.

Je pense que pour des cas comme celui-ci: visiter des arbres, yield fromrend le code plus simple et plus propre.

jimifiki
la source