1. ホーム
  2. python

リスト内包とジェネレータ式における降伏

2023-08-02 10:50:52

質問

以下の動作は、私にはむしろ直感に反しているように見えます(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 を生成しますが、3 行目は生成されません。

また、奇妙なことに

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

どなたかもっと情報を提供していただけないでしょうか。

どのように解決するのですか?

<ブロッククオート

注意 : これは CPython の yield を扱う CPython のバグで、 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 の 文法 はそれを許しますが (これがコードがコンパイル可能な理由です)、Python 3.7以前の yield 式の指定 を使用すると yield を使ってもうまくいかないことがわかります。

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

これは、バグであることが確認されている 問題 10544 . このバグの解決は yieldyield from を上げる SyntaxError Python 3.8 では Python 3.7では を発生させます。 DeprecationWarning を発生させ、コードがこの構成を使うのを止めるようにします。Python 2.7.15 以降で -3 コマンドラインスイッチ は、Python 3 互換性警告を有効にします。

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

YIELD_VALUE オペコードはそれぞれバイトコードインデックス 15 と 12 にあり、巣の中のカッコウのように余分なものです。つまり、リスト内包型ジェネレータでは、毎回スタックの先頭を生成する降伏が 1 回あります (スタックの先頭を yield の戻り値に置き換えます)、そしてジェネレータ式のバリアントでは、 スタックの一番上(整数)を yield してから また の戻り値がスタックに格納されます。 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 例外に添付されるのではなく、最後に 'yield' されます。

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