Le comportement suivant me semble plutôt contre-intuitif (Python 3.4):
>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]
Les valeurs intermédiaires de la dernière ligne ne sont en fait pas toujours None
, elles sont tout ce que nous send
dans le générateur, équivalent (je suppose) au générateur suivant:
def f():
for i in range(3):
yield (yield i)
Il me semble aussi drôle que ces trois lignes fonctionnent du tout. Le Référence dit que yield
n'est autorisé que dans une définition de fonction (bien que je puisse la lire mal et/ou qu'elle ait simplement été copiée de l'ancienne version). Les deux premières lignes produisent un SyntaxError
dans Python 2.7, mais pas la troisième ligne.
De plus, cela semble étrange
Quelqu'un pourrait-il fournir plus d'informations?
Remarque : il s'agissait d'un bogue dans la gestion par CPython de
yield
dans les compréhensions et les expressions de générateur, corrigé dans Python 3.8, avec un avertissement de dépréciation dans Python 3.7. Voir les rapport de bogue Python et Quoi de neuf entrées pour - Python 3.7 et Python 3.8 .
Les expressions de générateur et les compréhensions d'ensemble et de dict sont compilées en objets de fonction (générateur). Dans Python 3, les compréhensions de liste reçoivent le même traitement; elles sont toutes, par essence, une nouvelle portée imbriquée.
Vous pouvez voir ceci si vous essayez de démonter une expression de générateur:
>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
1 0 LOAD_CONST 0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
3 LOAD_CONST 1 ('<genexpr>')
6 MAKE_FUNCTION 0
9 LOAD_NAME 0 (range)
12 LOAD_CONST 2 (3)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 POP_TOP
23 LOAD_CONST 3 (None)
26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 11 (to 17)
6 STORE_FAST 1 (i)
9 LOAD_FAST 1 (i)
12 YIELD_VALUE
13 POP_TOP
14 JUMP_ABSOLUTE 3
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
Ce qui précède montre qu'une expression de générateur est compilée en un objet de code, chargée en fonction (MAKE_FUNCTION
crée l'objet fonction à partir de l'objet code). Le .co_consts[0]
référence nous permet de voir l'objet de code généré pour l'expression, et il utilise YIELD_VALUE
comme le ferait une fonction de générateur.
En tant que telle, l'expression yield
fonctionne dans ce contexte, car le compilateur les considère comme des fonctions déguisées.
Ceci est un bug; yield
n'a pas sa place dans ces expressions. Le Python grammaire avant Python 3.7 le permet (c'est pourquoi le code est compilable), mais le yield
spécification d'expression montre que l'utilisation de yield
ici ne devrait pas réellement fonctionner:
L'expression yield n'est utilisée que lors de la définition d'une fonction générateur et ne peut donc être utilisée que dans le corps d'une définition de fonction.
Il a été confirmé qu'il s'agissait d'un bogue dans problème 10544 . La résolution du bogue est qu'en utilisant yield
et yield from
sera augmentera un SyntaxError
dans Python 3.8 ; dans Python 3.7 il soulève un DeprecationWarning
pour garantir que le code cesse d'utiliser cette construction. Vous verrez le même avertissement dans Python 2.7.15 et plus si vous utilisez -3
commutateur de ligne de commande activation Python 3 avertissements de compatibilité.
L'avertissement 3.7.0b1 ressemble à ceci; transformer les avertissements en erreurs vous donne une exception SyntaxError
, comme vous le feriez en 3.8:
>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension
Les différences entre le fonctionnement de yield
dans une compréhension de liste et yield
dans une expression de générateur proviennent des différences dans la façon dont ces deux expressions sont implémentées. Dans Python 3 une compréhension de liste utilise LIST_APPEND
appelle pour ajouter le haut de la pile à la liste en cours de création, tandis qu'une expression de générateur renvoie à la place cette valeur. Ajout dans (yield <expr>)
ajoute simplement un autre YIELD_VALUE
opcode pour:
>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 13 (to 22)
9 STORE_FAST 1 (i)
12 LOAD_FAST 1 (i)
15 YIELD_VALUE
16 LIST_APPEND 2
19 JUMP_ABSOLUTE 6
>> 22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 12 (to 18)
6 STORE_FAST 1 (i)
9 LOAD_FAST 1 (i)
12 YIELD_VALUE
13 YIELD_VALUE
14 POP_TOP
15 JUMP_ABSOLUTE 3
>> 18 LOAD_CONST 0 (None)
21 RETURN_VALUE
Le YIELD_VALUE
l'opcode aux bytecode index 15 et 12 respectivement est extra, un coucou dans le nid. Donc, pour la liste-compréhension-devenue-générateur, vous avez 1 rendement produisant le haut de la pile à chaque fois (en remplaçant le haut de la pile par la valeur de retour yield
), et pour la variante d'expression du générateur, vous donnez le en haut de la pile (l'entier) et ensuite donner encore, mais maintenant la pile contient la valeur de retour de yield
et vous obtenez None
cette deuxième fois .
Pour la compréhension de la liste, la sortie d'objet list
prévue est toujours renvoyée, mais Python 3 voit cela comme un générateur, la valeur de retour est donc attachée à la StopIteration
exception comme attribut value
:
>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3)) # avoid exhausting the generator
[0, 1, 2]
>>> try:
... next(listgen)
... except StopIteration as si:
... print(si.value)
...
[None, None, None]
Ces objets None
sont les valeurs de retour des expressions yield
.
Et pour le répéter encore une fois; ce même problème s'applique au dictionnaire et à la compréhension des ensembles dans Python 2 et Python 3 également; dans Python 2 le yield
les valeurs de retour sont toujours ajoutées au dictionnaire ou à l'objet défini, et la valeur de retour est "renvoyée" en dernier au lieu d'être attachée à l'exception StopIteration
:
>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]