web-dev-qa-db-fra.com

rendement dans les listes de compréhension et les expressions de générateur

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

  • qu'une compréhension de liste renvoie un générateur et non une liste
  • et que l'expression de générateur convertie en liste et la compréhension de liste correspondante contiennent des valeurs différentes.

Quelqu'un pourrait-il fournir plus d'informations?

68
zabolekar

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])]
65
Martijn Pieters