👌

ジェネレータでyieldの後に処理が書いてあったらどうなる

2023/08/31に公開

はじめに

どうも、うみぶどうです。エックスポイントワンで働き始めてまだ日が浅いエンジニアです。
Pythonのジェネレータの動きについて、理解が足りず困った経験から記事を執筆しました。
表題通りです。どうなるんでしょう。

そもそもジェネレータってなに

Pythonにおいて、繰り返し値を返却することができる関数のことです。
ものすごくざっくり説明すると、関数で値の返却を記述する局面で、

return value

とするところをreturnの代わりに、yieldを以下のように使用します。

for row in rows:
    yield row

呼び出される度に一つずつ、値を返却します。
返却する値を一つずつ扱うことでメモリ利用の点でメリットがあります。

※forループを使わず複数のyieldを配置している関数もジェネレータとして機能しますが、
ここでの説明は割愛します。

どうやって使うの

外部から呼び出すことで結果を取得できます。
一般的に1件取得する場合は

result = next(generater(), None)

というかたちが多く、

複数件(全件)取得する場合は
forループを利用して処理したり、

result = list(generater())

メモリ利用の観点から懸念はありますがリストで受け取ったり、
という方法があります。

yieldしたら呼び出し元に処理が移る

1件取得の処理を書いたら、どうもジェネレータに書かれた処理が最後まで実行されていない。
それが原因でエラーが起きているようだ、という経験がありました。

ジェネレータである関数を呼び出すと、ジェネレータ内部の処理が実行されます。
ジェネレータ内部の処理がyieldまで到達すると、呼び出し元に処理が戻ります、
Pythonは並列処理ではないので、ジェネレータ内の処理は中断します。
再度呼び出さなければどうなるか。そのまま実行されずに放置されます。

yieldの後に処理があるとどうなるか

例えば、ジェネレータが返却すべき値を3個持っている場合を考えてみましょう。
3回きっちりジェネレータから値を受け取った場合、どうなるでしょうか。

ジェネレータ外部の処理 ジェネレータ内の処理 ジェネレータの状況
ジェネレータを呼ぶ以前 開始されていない
ジェネレータの呼び出し
ジェネレータの処理開始 先頭から処理がスタート
ジェネレータが値の返却(1回目) 最初のyieldに到達
ジェネレータから受け取り 最初のyieldで処理が中断
ジェネレータの呼び出し(2回目)
ジェネレータの処理再開 yield後から処理リスタート
ジェネレータが値の返却(2回目) 2回目のyieldに到達
ジェネレータから受け取り yieldで処理が中断
ジェネレータの呼び出し(3回目)
ジェネレータの処理再開 yield後から処理リスタート
ジェネレータが値の返却(3回目) 最後に書かれたyield到達
ジェネレータから受け取り 最後に書かれたyieldで中断
その後の処理を継続 最後のyieldより後の処理は実行されないまま

今回のケースでわかったジェネレータ利用の注意点

もしもジェネレータに最後のyield文よりも後に処理が書いてあった場合、
1件取得する時はnext(generater(),None)で処理してやればいいんだ、
と、なんとなくで扱うとジェネレータの処理が最後まで完了しない、ということが起こり得ます。

最後のyield文より後の処理も実行したければ、ジェネレータの返却回数(yieldでの中断回数)
n回にプラス1回呼び出してやらなければ、ジェネレータの処理が最後まで完了しないわけです。
yieldでの返却回数が1回なら、プラス1の2回呼ばないといけないんですね。
もしくはループなどでジェネレータを全件処理すればOKです。

最後に

この記事を書こうと思ったのは、yieldの後に処理が書いてあった場合についての挙動を、
検索などでなかなか見つけることができなかったからです。

そのような記事があまり多くないということはもしかしたら、
そもそもジェネレータでyieldより後には処理を書かないほうがいいのか、などと考えてもしまいますが。
同じように困っている誰かの助けになればと思って書きました。以上です。

エックスポイントワン技術ブログ

Discussion