注: これは 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 でyield
andyield 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])]