686 votes

En pratique, quelles sont les principales utilisations de la nouvelle syntaxe "yield from" de Python 3.3 ?

J'ai du mal à me faire à l'idée que PEP 380 .

  1. Quelles sont les situations où le "rendement de" est utile ?
  2. Quel est le cas d'utilisation classique ?
  3. Pourquoi le compare-t-on aux micro-filets ?

[mise à jour]

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

IMHO les coroutines sont la fonctionnalité la plus obscure de Python la plupart des livres le font paraître inutile et inintéressant.

Merci pour les grandes réponses, mais un merci spécial à agf et son commentaire lié à Présentations de David Beazley . David rock.

37 votes

16 votes

Vidéo de l'intervention de David Beazley dabeaz.com/coroutines présentation : youtube.com/watch?v=Z_OAlIhXziw

1030voto

Praveen Gollakota Points 8440

Mettons d'abord une chose au clair. L'explication que yield from g est équivalent à for v in g: yield v ne commence même pas à rendre justice à quoi yield from est tout ce qui compte. Parce que, regardons les choses en face, si tout yield from permet d'étendre le for alors il n'est pas nécessaire d'ajouter yield from au langage et empêchent l'implémentation d'un grand nombre de nouvelles fonctionnalités dans Python 2.x.

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

  • La connexion est "transparente" dans le sens où elle propage également tout ce qui est correct, et 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 à la fois envoyées de y à un générateur.

( Si nous parlions de TCP, yield from g pourrait signifier "maintenant déconnecter temporairement la socket de mon client et la reconnecter à cette autre socket du serveur". )

BTW, si vous n'êtes pas sûr de ce que envoi de données à un générateur même signifie, que vous devez tout laisser tomber et lire sur coroutines tout d'abord, ils sont très utiles (contraste avec les sous-programmes ), mais malheureusement moins connu en Python. Le cours curieux de Dave Beazley sur les coroutines est un excellent début. Lire les diapositives 24-33 pour une amorce 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 sur reader() on peut juste yield from il.

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 qui ne change la vie.

Envoyer des données à un générateur (coroutine) en utilisant yield from - Partie 1

Maintenant, faisons quelque chose de plus intéressant. Créons une coroutine appelée writer qui 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 wrapper doit gérer l'envoi de données au rédacteur, de sorte que toute donnée envoyée au wrapper soit de manière transparente envoyé à la 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

Le wrapper doit accepter les données qui lui sont envoyées (évidemment) et doit également traiter les StopIteration lorsque la boucle for est épuisée. De toute évidence, il suffit de faire for x in coro: yield x ne fonctionne 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, on pourrait faire ça.

def writer_wrapper(coro):
    yield from coro

Cela permet d'économiser 6 lignes de code, de le rendre beaucoup plus lisible et de le faire fonctionner. Magique !

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

Rendons les choses plus compliquées. Et si notre rédacteur doit gérer des exceptions ? Disons que le writer manipule un SpamException et il imprime *** s'il en rencontre un.

class SpamException(Exception):
    pass

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

Et si nous ne changeons 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

Um, ça ne marche pas parce que x = (yield) lève juste l'exception et tout s'arrête net. Faisons en sorte que cela fonctionne, mais en traitant 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

Ça marche.

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

Mais ça aussi !

def writer_wrapper(coro):
    yield from coro

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

Mais cela ne couvre pas encore tous les cas de figure. Que se passe-t-il si le générateur externe est fermé ? Et dans le 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 ? Ce yield from gère de manière transparente tous les cas de figure, c'est vraiment impressionnant. . yield from fonctionne comme par magie et gère tous ces cas.

Je pense personnellement yield from est un mauvais choix de mot-clé parce qu'il ne fait pas de la bidirectionnel nature apparente. D'autres mots-clés ont été proposés (comme delegate mais ont été rejetés parce qu'il est beaucoup plus difficile d'ajouter un nouveau mot clé à la langue que de combiner des mots clés existants.

En résumé, il est préférable de penser à yield from en tant que transparent two way channel entre l'appelant et le sous-générateur.

Références :

  1. PEP 380 - Syntaxe pour déléguer à 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]

3 votes

@PraveenGollakota, dans la deuxième partie de votre question, Envoyer des données à un générateur (coroutine) en utilisant yield from - Partie 1 Que faire si vous avez plus de coroutines vers lesquelles transférer l'élément reçu ? Comme un scénario de diffuseur ou d'abonné où vous fournissez plusieurs coroutines au wrapper dans votre exemple et les éléments doivent être envoyés à tous ou à un sous-ensemble d'entre eux ?

2 votes

En faisant except StopIteration: pass À l'intérieur de la while True: n'est pas une représentation exacte de la yield from coro - qui n'est pas une boucle infinie et après coro est épuisé (c'est-à-dire qu'il déclenche StopIteration), writer_wrapper exécutera l'instruction suivante. Après la dernière instruction, il s'auto-relève lui-même. StopIteration comme tout générateur épuisé...

1 votes

...donc si writer contenu for _ in range(4) au lieu de while True puis après l'impression >> 3 cela permettrait AUSSI de relancer automatiquement StopIteration et cela serait géré automatiquement par yield from et ensuite writer_wrapper relèverait automatiquement son propre StopIteration et parce que wrap.send(i) n'est pas à l'intérieur try il serait en fait soulevé à ce moment-là (c'est-à-dire que le suivi ne signalera que la ligne avec wrap.send(i) et non pas de l'intérieur du générateur)

137voto

Niklas B. Points 40619

Quelles sont les situations où le "rendement de" est 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 lui manque plusieurs aspects, en particulier la gestion correcte de l'attribut .throw() / .send() / .close() mécanismes introduits par PEP 342 . Pour faire cela correctement, assez compliqué est nécessaire.

Quel est le cas d'utilisation classique ?

Considérons que vous voulez extraire des informations d'une structure de données récursive. Disons que nous voulons obtenir tous les nœuds de feuilles d'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'existait pas de méthode simple pour remanier 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 en générateurs distincts. Sans yield from C'est laid, à tel point que vous réfléchirez à deux fois si vous voulez vraiment le faire. Avec yield from c'est en fait agréable à 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 le compare-t-on aux micro-filets ?

Je pense que ce cette section dans le PEP est de dire que chaque générateur a son propre contexte d'exécution isolé. Si l'on ajoute à cela le fait que l'exécution est commutée entre le générateur-itérateur et l'appelant à l'aide de la fonction yield y __next__() Ceci est similaire aux threads, où le système d'exploitation change de temps en temps le thread en cours d'exécution, ainsi que le contexte d'exécution (pile, registres, ...).

L'effet de cette mesure est également comparable : Le générateur-itérateur et l'appelant progressent tous deux 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'a rien de spécifique à yield from Il s'agit plutôt d'une propriété générale des générateurs en Python.

0 votes

Le remaniement des générateurs est douloureux aujourd'hui.

1 votes

J'ai tendance à utiliser itertools beaucoup pour refactorer les générateurs (des trucs comme itertools.chain), ce n'est pas si important. J'aime bien yield from, mais je ne vois toujours pas en quoi il est révolutionnaire. Il l'est probablement, puisque Guido en est fou, mais je dois manquer la grande image. Je suppose que c'est génial pour send() car c'est difficile à refactorer, mais je ne l'utilise pas souvent.

0 votes

Je suppose que ces get_list_values_as_xxx sont des générateurs simples avec une seule ligne for x in input_param: yield int(x) et les deux autres respectivement avec str y float

42voto

ospider Points 1507

Un court exemple vous aidera à comprendre l'une des yield from Cas d'utilisation : obtenir une 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]]])))

10 votes

Je voulais juste suggérer que l'impression à la fin aurait l'air un peu plus jolie sans la conversion en liste print(*flatten([1, [2], [3, [4]]]))

40voto

Ben Jackson Points 28358

Chaque fois que vous invoquez un générateur à l'intérieur d'un générateur, vous avez besoin d'une "pompe" pour réintroduire les données dans le générateur. yield les valeurs : for v in inner_generator: yield v . Comme le souligne le PEP, il existe des complexités subtiles que la plupart des gens ignorent. Le contrôle de flux non-local comme throw() est un exemple donné dans le PEP. La nouvelle syntaxe yield from inner_generator est utilisé là où vous auriez écrit l'explicite for avant. Il ne s'agit pas simplement d'un sucre syntaxique : il gère tous les cas de figure qui sont ignorés par la méthode de l'arbre. for boucle. Le fait d'être "sucré" encourage les gens à l'utiliser et donc à obtenir les bons comportements.

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

Avec les caractéristiques supplémentaires du générateur introduites par le PEP 342, ce n'est plus le cas. plus le cas : comme décrit dans le PEP de Greg, l'itération simple ne supporte pas supporte pas correctement send() et throw(). La gymnastique nécessaire pour supporter send() et throw() ne sont en fait pas si complexes que ça quand on les décompose mais elle n'est pas non plus triviale.

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

Le nouveau yield from n'ajoute aucune capacité supplémentaire au langage en termes de threading, elle facilite simplement l'utilisation correcte des fonctionnalités existantes. Ou plus précisément, elle facilite l'utilisation d'une novice consommateur d'un générateur interne complexe écrit par un expert pour passer à travers ce générateur sans briser aucune de ses caractéristiques complexes.

13voto

kat1330 Points 3000

yield donnera une seule valeur dans la collection.

yield from rendra la collection en collection et la rendra plate.

Regardez cet exemple :

def yieldOnly():
    yield "A"
    yield "B"
    yield "C"

def yieldFrom():
    for i in [1, 2, 3]:
        yield from yieldOnly()

test = yieldFrom()
for i in test:
print(i)

Dans la console, vous verrez :

A
B
C
A
B
C
A
B
C

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X