É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 from
tout. Car, avouons-le, si tout ce yield from
qui est fait est d'étendre la for
boucle, alors cela ne justifie pas d'ajouter yield from
au 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 g
pourrait 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 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 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 StopIteration
moment où la boucle for est épuisée. Évidemment, faire for x in coro: yield x
ne 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 writer
poignées a SpamException
et 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 from
gè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 from
gestion transparente de tous les cas d'angle est vraiment impressionnante . yield from
fonctionne comme par magie et gère tous ces cas.
Personnellement, je pense que yield from
c'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 delegate
mais 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 from
comme transparent two way channel
entre l'appelant et le sous-générateur.
Références:
- PEP 380 - Syntaxe de délégation à un sous-générateur (Ewing) [v3.3, 2009-02-13]
- PEP 342 - Coroutines via des générateurs améliorés (GvR, Eby) [v2.5, 2005-05-10]