J'ai de la difficulté à comprendre ce que je pense PEP 38 .
[ mise à jour ]
Maintenant je comprends la cause de mes difficultés. J'ai utilisé des générateurs, mais jamais vraiment utilisé de coroutines (introduite par PEP-342 ). Malgré certaines similitudes, les générateurs et les routines sont deux concepts différents. Comprendre les coroutines (pas seulement les générateurs) est la clé pour comprendre la nouvelle syntaxe.
Les routines IMHO sont la fonction la plus obscure de Python , la plupart des livres le rendent inutile et sans intérêt.
Merci pour les bonnes réponses, mais merci tout spécialement à agf et son commentaire renvoyant à présentations de David Beazley . David bascule.
Faisons d'abord une chose de la route. 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 sur. Parce que, admettons-le, si tout ce que _yield from
_ fait est de développer la boucle for
, cela ne garantit pas l'ajout de _yield from
_ au langage et empêche toute une série de nouvelles fonctionnalités d'être implémentées dans Python 2.x.
Ce que _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 tout correctement également, 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 from et to un générateur.
(Si nous parlions de TCP, _yield from g
_ pourrait signifier "maintenant 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 envoyer des données à un générateur signifie même, vous devez tout laisser tomber et lire sur coroutines d'abord - ils sont très utiles (comparez-les avec sous-programmes), mais malheureusement moins connu en Python. Le cours curieux de Dave Beazley sur Couroutines est un excellent début. Lisez les diapositives 24 à pour une introduction rapide.
_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()
, nous pouvons simplement _yield from
_.
_def reader_wrapper(g):
yield from g
_
Cela fonctionne et nous avons éliminé une ligne de code. Et probablement l'intention est un peu plus claire (ou pas). Mais rien ne change la vie.
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'encapsulation doit gérer l'envoi de données à l'écrivain, de sorte que toute donnée envoyée à l'encapsuleur soit de manière transparente envoyée à 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 gérer le StopIteration
lorsque la boucle for est épuisée. Évidemment, le fait de faire _for x in coro: yield x
_ ne suffira 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 économise 6 lignes de code, le rend beaucoup plus lisible et fonctionne. La magie!
Rendons les choses plus compliquées. Et si notre auteur avait besoin de gérer les exceptions? Supposons que writer
gère un SpamException
et qu'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 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
_
Euh, ça ne marche pas parce que x = (yield)
ne fait que lever l'exception et que tout s'arrête brusquement. Faisons-le fonctionner, mais gérons manuellement les exceptions et les envoyons ou les jettent 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 c'est pareil!
_def writer_wrapper(coro):
yield from coro
_
_yield from
_ gère de manière transparente l’envoi des valeurs ou leur émission dans le sous-générateur.
Cela ne couvre toujours pas tous les cas d'angle cependant. Que se passe-t-il si le générateur externe est fermé? Qu'en est-il du cas où le sous-générateur renvoie une valeur (oui, dans 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 coin est vraiment impressionnant . _yield from
_ fonctionne et gère tous ces cas comme par magie.
Personnellement, j'estime que _yield from
_ est un mauvais choix de mot clé, car il 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 la combinaison de ceux 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:
Quelles sont les situations où "céder" 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 assez naïve d'utilisation du sous-générateur. Il lui manque plusieurs aspects, en particulier le traitement correct des mécanismes .throw()
/.send()
/.close()
introduits par PEP 342 . Pour faire cela correctement, plutôt compliqué le code 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 de feuille dans un arbre:
def traverse_tree(node):
if not node.children:
yield node
for child in node.children:
yield from traverse_tree(child)
Encore plus important est le fait que jusqu’au yield from
, il n’existait 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)
Maintenant, vous décidez de factoriser ces boucles dans des générateurs distincts. Sans yield from
, c'est moche, jusqu'au point où vous réfléchirez à deux fois si vous voulez réellement le faire. Avec yield from
, il est intéressant 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 cette section du PEP parle, c’est que chaque générateur a son propre contexte d’exécution isolé. Avec le fait que l'exécution est commutée entre le générateur-itérateur et l'appelant à l'aide de yield
et __next__()
, respectivement, ceci est similaire aux threads, où le système d'exploitation bascule le thread en cours d'exécution de temps en temps, avec l'exécution contexte (pile, registres, ...).
L'effet obtenu est également comparable: le générateur-itérateur et l'appelant progressant simultanément dans leur état d'exécution, leurs exécutions sont imbriquées. Par exemple, si le générateur effectue un type de calcul et que l'appelant imprime les résultats, ceux-ci seront affichés dès qu'ils seront disponibles. C'est une forme de concurrence.
Cette analogie n’a rien de spécifique à yield from
, c’est plutôt une propriété générale des générateurs en Python.
Où que vous appeliez un générateur à l'intérieur d'un générateur, vous avez besoin d'une "pompe" pour re_yield
les valeurs: for v in inner_generator: yield v
. Comme le souligne le PPE, il existe des complexités subtiles que la plupart des gens ignorent. Un contrôle de flux non local comme throw()
est un exemple donné dans le PEP. La nouvelle syntaxe yield from inner_generator
est utilisée partout où vous auriez écrit la boucle explicite for
. Cependant, il ne s’agit pas simplement d’un sucre syntaxique: il gère tous les cas de passage ignorés par la boucle for
. Être "sucré" encourage les gens à l'utiliser et à adopter 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, la simple itération ne prend pas correctement en charge les méthodes send () et throw (). La gymnastique nécessaire pour supporter send () et throw () n'est en réalité pas si complexe lorsque vous les décomposez, mais elles ne sont pas non plus anodines.
Je ne peux pas parler d'une comparaison avec des micro-threads, autre que de constater que les générateurs sont un type de paralellisme. Vous pouvez considérer le générateur suspendu comme un thread qui envoie des valeurs via yield
à un thread grand public. L'implémentation réelle ne ressemble peut-être pas à cela (et son implémentation présente évidemment un grand intérêt pour les développeurs Python), mais cela ne concerne pas les utilisateurs.
La nouvelle syntaxe yield from
n'ajoute aucune fonctionnalité supplémentaire au langage en termes de threading, elle facilite simplement l'utilisation correcte des fonctionnalités existantes. Ou plus précisément, il est plus facile pour un novice consommateur d'un générateur interne complexe écrit par un expert pour passer à travers ce générateur sans casser aucune de ses fonctionnalités complexes.
Un court exemple vous aidera à comprendre l'un des cas d'utilisation de yield from
: 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]]])))
En utilisation appliquée pour asynchrone IO coroutine , _yield from
_ a un comportement similaire à await
dans un fonction de coroutine . Qui sont tous deux utilisés pour suspendre l'exécution de la coroutine.
_yield from
_ est utilisé par le coroutine basée sur un générateur .
await
est utilisé pour async def
coroutine. (depuis Python 3.5+)
Pour Asyncio, s'il n'est pas nécessaire de prendre en charge une version antérieure de Python (c'est-à-dire> 3.5), _async def
_/await
est la syntaxe recommandée pour définir une coroutine. Ainsi, _yield from
_ n'est plus nécessaire dans une coroutine.
Mais en général en dehors de l'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.
yield from
enchaîne 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 constater, il supprime une boucle pure Python. C'est à peu près tout ce que cela fait, mais l'enchaînement d'itérateurs est un modèle assez courant en Python.
Les threads sont essentiellement une fonctionnalité qui vous permet de quitter des fonctions à des points complètement aléatoires et de revenir dans l'état d'une autre fonction. Le superviseur de threads le fait très souvent, ainsi 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 ressemblent beaucoup aux threads dans ce sens: ils vous permettent de spécifier des points spécifiques (à chaque fois qu'ils yield
) où vous pouvez entrer et sortir. Lorsqu'ils sont utilisés de cette manière, les générateurs sont appelés coroutines.
Lisez cet excellent tutoriel sur les coroutines dans Python pour plus de détails