Python 3.11.5
Pythonのジェネレータは、一言でいえば「イテレータを生成する機能」です。使ったことがなくとも、コードリーディングをしているときにときどき見かけるので、覚えておくと便利な機能となります。
yield
はジェネレータ関数を作成するときに使う「処理を終了せず返り値を返す予約語」のことです。
実際に手を動かしてその機能を紹介していきます。
ジェネレータの説明を始める前に、yield
とreturn
の違いを簡単に説明します。
yield
関数の処理を停止して値を返すreturn
関数の処理を終了して値を返すyield
が「停止」となっているのは、「再開」が可能だからです。
まずは簡単なジェネレータ関数の例を作ってみます。
このスクリプトを実行すると、以下のような結果が得られます。
1つ目のジェネレータオブジェクトがいわゆる「繰り返せるオブジェクト(イテラブルオブジェクト)」であり、for
文で取り出すことも可能です。
以下でも1, 2, 3
の結果が得られます。
PythonのList型へ変換したい場合はlist()
関数を使います。
結果は以下です。
一度生成したジェネレータの中にオブジェクトを追加する場合はsend()
関数を使います。
これの出力結果は以下です。
この処理の流れを説明すると以下のようになります。
next()
で2
が返されるsend()
で値が送られた場合、変数v
に格納されるsend()
で値を送った場合、変数result
に格納されるresult
まで返し終わった後にsend()
しようとしてもエラーが出るジェネレータに内包されるオブジェクトの数に限りがある場合、次の要素を取り出すnext()
関数を限界以上に実行するとエラーが出ます。
結果は以下です。
エラーハンドリングしたい場合は以下のように書きます。
for
文などで繰り返し処理でオブジェクトを処理する場合、このように例外発生でbreak
するなどします。
ジェネレータ内でreturn
を使って値を返した場合、StopIteration
例外が発生します。
これが出力する結果は以下です。
print(gen.send(8))
以降の処理はreturn
されているため発生しません。
List型と同じく、内包表記でジェネレータを書けます。List型だと[]
ですが、ジェネレータの場合は()
で囲みます。
これは1 ~ 11の範囲で階乗のジェネレータを作成し、5までの取り出したケースとなります。出力結果は以下です。
ほとんど内包表記で書くことはありませんが、こういった使い方も可能です。
今までジェネレータについて紹介してきましたが、「一体いつ使うのか」「List型でよくないか」と感じる人もいることでしょう。
いくつかのユースケースを紹介します。
たとえば、巨大なCSVファイルがあったとします。これをreturn
で返す場合とyield
で返す場合(ジェネレータ関数)で比較してみましょう。
return
を使った関数がList型変数data
へ行を追加して返すのに対し、yield
を使うと行ごとに返せます。
これはメモリ消費量の観点で非常に効率的です。
次にデータベース接続をするケースを見てみましょう。Pythonのデータベース系ライブラリのSQLAlchemy
を使います。
データベース接続でreturn
ではなくyield
を使う理由は、CSVファイルと同じく多大なデータ量になった際、メモリを効率的に使うためです。
データベース接続によって得られるデータは、たいていの場合はイテラブル(繰り返し処理できるオブジェクト)なのでジェネレータが有効となります。
他にもジェネレータの利用が有効なシーンはたくさんあります。コードを書くと長くなるため、以下に列挙します。
なかなか使うシーンを想像しづらいジェネレータですが、多くのWebアプリケーションフレームワークやライブラリで処理の効率化を考慮して利用されています。
自身で実装するアプリケーションやツールでも、効率的に利用できるケースがあったら積極的に使っていきましょう。
メモリ消費を抑える実装は、アルゴリズムや設計といった基礎的なスキル以外にも、こういった言語の機能でサポートされていることがあるので、ぜひ覚えておきたいところです。