79

次の動作は、私にとってかなり直感に反するようです (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]

最後の行の中間値は、実際には常にNoneではなくsend、ジェネレーターに入力したものであり、次のジェネレーターと同等 (推測) です。

def f():
   for i in range(3):
      yield (yield i)

これらの 3 つの行がまったく機能するのはおかしいと思います。リファレンスには、yield関数定義でのみ許可されていると書かれています(ただし、間違って読んでいる可能性や、単に古いバージョンからコピーされた可能性があります)。最初の 2 行SyntaxErrorは Python 2.7 で a を生成しますが、3 行目は生成しません。

また、奇妙に思える

  • リスト内包表記はリストではなくジェネレーターを返すこと
  • また、リストに変換されたジェネレータ式と対応するリスト内包表記には異なる値が含まれています。

誰かがより多くの情報を提供できますか?

4

1 に答える 1

80

: これは CPython のyield内包表記とジェネレーター式の処理のバグであり、Python 3.8 で修正され、Python 3.7 で非推奨の警告が表示されました。Python バグ レポートPython 3.7およびPython 3.8の新機能のエントリを参照してください。

ジェネレーター式、set および dict 内包表記は (ジェネレーター) 関数オブジェクトにコンパイルされます。Python 3 では、リスト内包表記は同じように扱われます。それらはすべて、本質的に、新しいネストされたスコープです。

ジェネレータ式を逆アセンブルしようとすると、次のようになります。

>>> 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

上記は、ジェネレーター式がコード オブジェクトにコンパイルされ、関数として読み込まれることを示しています (MAKE_FUNCTIONコード オブジェクトから関数オブジェクトを作成します)。.co_consts[0]参照により、式に対して生成されたコード オブジェクトを確認できます。これは、ジェネレーター関数と同じように使用されますYIELD_VALUE

そのためyield、コンパイラはこれらを変装した関数と見なすため、式はそのコンテキストで機能します。

これはバグです。yieldは、これらの表現では使用できません。Python 3.7 より前の Python文法では許可されていましたが (これがコードがコンパイル可能である理由です)、yield式の仕様yieldは、ここでの使用は実際には機能しないことを示しています。

yield 式は、ジェネレーター関数を定義するときにのみ使用されるため、関数定義の本体でのみ使用できます。

これはissue 10544のバグであることが確認されています。バグの解決策は、Python 3.8 でyieldandyield fromを使用するとaSyntaxErrorが発生することです。Python 3.7では、DeprecationWarningコードがこの構造の使用を確実に停止するために a が発生します。Python 3 互換性警告を有効にする-3コマンド ライン スイッチを使用すると、Python 2.7.15 以降で同じ警告が表示されます。

3.7.0b1 の警告は次のようになります。警告をエラーに変えるSyntaxErrorと、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

yieldリスト内包表記とジェネレータ式の動作の違いは、これら 2 つの式の実装方法の違いyieldに起因します。Python 3 では、リスト内包表記はLIST_APPEND呼び出しを使用してスタックの一番上を構築中のリストに追加しますが、代わりにジェネレーター式がその値を生成します。追加する(yield <expr>)と、別のYIELD_VALUEオペコードが次のいずれかに追加されます。

>>> 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

YIELD_VALUEバイトコード インデックス 15 と 12 のそれぞれのオペコードは余分で、巣の中のカッコウです。したがって、リスト内包表記ジェネレーターの場合、毎回スタックのトップを生成する 1 つの yield があり (スタックのトップをyield戻り値で置き換えます)、ジェネレーター式バリアントの場合は、スタックのトップを生成します ( integer) その後、もう一度yield しますが、スタックには の戻り値が含まれており、2 回目のyield取得Noneが行われます。

リスト内包表記では、意図したlistオブジェクト出力が引き続き返されますが、Python 3 はこれをジェネレーターと見なすため、代わりに戻り値が属性としてStopIteration例外に添付されます。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]

これらNoneのオブジェクトは、yield式からの戻り値です。

これをもう一度繰り返します。これと同じ問題が、Python 2 と Python 3 の辞書と集合内包表記にも当てはまります。Python 2 では、戻り値は意図したディクショナリまたはセット オブジェクトに引き続き追加され、戻り値は例外yieldに添付されるのではなく、最後に「生成」されます。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])]
于 2015-08-21T12:08:38.100 に答える