web-dev-qa-db-ja.com

リスト内包表記とジェネレータ式での収量

次の動作は、私にはかなり直感的ではないようです(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 in Python 2.7が生成されますが、3行目では生成されません。

また、奇妙なようです

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

誰かがもっと情報を提供できますか?

68
zabolekar

:これは、CPythonの内包表記およびジェネレータ式でのyieldの処理のバグであり、Python 3.8で修正されました。 Python 3.7で非推奨の警告が表示されます。 Pythonバグレポート およびWhat's NewPython 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の前のPythongrammarで許可されています(これがコードをコンパイルできる理由です)が、 yield expression specification は、ここでyieldを使用しても実際には機能しないことを示しています。

Yield式は、generator関数を定義するときにのみ使用されるため、関数定義の本体でのみ使用できます。

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

3.7.0b1警告は次のようになります。警告をエラーに変換すると、3.8の場合と同様にSyntaxError例外が発生します。

>>> [(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とジェネレータ式のyieldの動作の違いは、これら2つの式の実装方法の違いに起因しています。 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

バイトコードインデックス15および12のYIELD_VALUEオペコードは、それぞれ余分なものであり、ネストのカッコウです。したがって、list-comprehension-turned-generatorの場合、毎回スタックの最上部を生成する1つのyield(スタックの最上部をyield戻り値に置き換える)を持ち、ジェネレータ式のバリアントに対してスタックの最上部(整数)を取得し、againを生成しますが、スタックにはyieldの戻り値が含まれ、Noneその2回目。

リスト内包の場合、目的のlistオブジェクトの出力が引き続き返されますが、Python 3はこれをジェネレーターと見なし、代わりに戻り値が StopIteration exceptionvalue属性として:

>>> 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例外に付加されるのではなく、最後に 'yielded'になります。

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