Pythonのジェネレータは、一言でいえば「イテレータを生成する機能」です。使ったことがなくとも、コードリーディングをしているときにときどき見かけるので、覚えておくと便利な機能となります。
yield
はジェネレータ関数を作成するときに使う「処理を終了せず返り値を返す予約語」のことです。
イテレータは「繰り返して一つずつ取り出せるオブジェクト」です。
実際に手を動かしてその機能を紹介していきます。
yieldとreturnの違い
ジェネレータの説明を始める前に、yield
とreturn
の違いを簡単に説明します。
yield
関数の処理を停止 して値を返す
return
関数の処理を終了 して値を返す
yield
が「停止」となっているのは、「再開」が可能だからです。
ジェネレータ関数を作ってみる
まずは簡単なジェネレータ関数の例を作ってみます。
def sample ( ) :
yield 1
yield 2
yield 3
gen = sample ( )
print ( gen )
print ( type ( gen ) )
print ( next ( gen ) )
print ( next ( gen ) )
print ( next ( gen ) )
このスクリプトを実行すると、以下のような結果が得られます。
< generator object sample at 0x104bf09e 0 >
< class 'generator' >
1
2
3
1つ目のジェネレータオブジェクトがいわゆる「繰り返せるオブジェクト(イテラブルオブジェクト)」であり、for
文で取り出すことも可能です。
以下でも1, 2, 3
の結果が得られます。
def sample ( ) :
yield 1
yield 2
yield 3
gen = sample ( )
[ print ( i ) for i in gen ]
List(配列)型へ変換
PythonのList型へ変換したい場合はlist()
関数を使います。
def sample ( ) :
yield 1
yield 2
yield 3
gen = list ( sample ( ) )
print ( type ( gen ) )
print ( gen )
結果は以下です。
ジェネレータにオブジェクトを追加する
一度生成したジェネレータの中にオブジェクトを追加する場合はsend()
関数を使います。
def sample ( ) :
v = yield 2
result = yield v
yield result
gen = sample ( )
print ( next ( gen ) )
print ( gen . send ( 4 ) )
print ( gen . send ( 8 ) )
print ( gen . send ( 16 ) )
これの出力結果は以下です。
2
4
8
Traceback ( most recent call last ) :
File "sample.py" , line 10 , in < module >
print ( gen.send ( 16 ))
^^^^^^^^^^^^
StopIteration
この処理の流れを説明すると以下のようになります。
ジェネレータ関数を実行する
最初のnext()
で2
が返される
次の処理からsend()
で値が送られた場合、変数v
に格納される
さらに次にsend()
で値を送った場合、変数result
に格納される
result
まで返し終わった後にsend()
しようとしてもエラーが出る
ジェネレータのエラー処理
ジェネレータに内包されるオブジェクトの数に限りがある場合、次の要素を取り出すnext()
関数を限界以上に実行するとエラーが出ます。
def sample ( ) :
yield 1
yield 2
yield 3
gen = sample ( )
print ( next ( gen ) )
print ( next ( gen ) )
print ( next ( gen ) )
print ( next ( gen ) ) # ここでエラーが出る
結果は以下です。
1
2
3
Traceback ( most recent call last ) :
File "sample.py" , line 10 , in < module >
print ( next ( gen ))
^^^^^^^^^
StopIteration
エラーハンドリングしたい場合は以下のように書きます。
def sample ( ) :
yield 1
yield 2
yield 3
gen = sample ( )
for i in range ( 4 ) :
try :
print ( next ( gen ) )
except StopIteration as e :
print ( 'StopIteration raised' )
break
for
文などで繰り返し処理でオブジェクトを処理する場合、このように例外発生でbreak
するなどします。
ジェネレータでreturnした場合
ジェネレータ内でreturn
を使って値を返した場合、StopIteration
例外が発生します。
def sample ( ) :
v = yield 2
result = yield v
if result != 4 :
return 'Error' # 文字列でエラー内容を返した場合
yield result
gen = sample ( )
print ( next ( gen ) )
print ( gen . send ( 4 ) )
print ( gen . send ( 8 ) )
これが出力する結果は以下です。
2
4
Traceback ( most recent call last ) :
File "/Users/kt2763/tmp/frkz-python-generator/sample.py" , line 13 , in < module >
print ( gen.send ( 8 ))
^^^^^^^^^^^
StopIteration: Error
print(gen.send(8))
以降の処理はreturn
されているため発生しません。
ジェネレータの内包表記
List型と同じく、内包表記でジェネレータを書けます。List型だと[]
ですが、ジェネレータの場合は()
で囲みます。
squares = ( x ** 2 for x in range ( 1 , 11 ) )
for i in range ( 5 ) :
print ( next ( squares ) )
これは1 ~ 11の範囲で階乗のジェネレータを作成し、5までの取り出したケースとなります。出力結果は以下です。
ほとんど内包表記で書くことはありませんが、こういった使い方も可能です。
ジェネレータの使い所
今までジェネレータについて紹介してきましたが、「一体いつ使うのか」「List型でよくないか」と感じる人もいることでしょう。
いくつかのユースケースを紹介します。
大きなファイルの中身を取り出すとき
たとえば、巨大なCSVファイルがあったとします。これをreturn
で返す場合とyield
で返す場合(ジェネレータ関数)で比較してみましょう。
import csv
def read_csv_file_with_return ( file_path ) :
with open ( file_path , 'r' ) as file :
data = [ ]
for row in csv . reader ( file ) :
data . append ( row )
return data
def read_csv_file_with_yield ( file_path ) :
with open ( file_path , 'r' ) as file :
for row in csv . reader ( file ) :
yield row
return
を使った関数がList型変数data
へ行を追加して返すのに対し、yield
を使うと行ごとに返せます。
これはメモリ消費量の観点で非常に効率的です。
データベース接続を保持するとき
次にデータベース接続をするケースを見てみましょう。Pythonのデータベース系ライブラリのSQLAlchemy
を使います。
import sqlalchemy as db
def get_db ( ) :
engine = db . create_engine ( 'postgresql://user:password@localhost/mydatabase' )
connection = engine . connect ( )
try :
yield connection
finally :
connection . close ( )
データベース接続でreturn
ではなくyield
を使う理由は、CSVファイルと同じく多大なデータ量になった際、メモリを効率的に使うためです。
データベース接続によって得られるデータは、たいていの場合はイテラブル(繰り返し処理できるオブジェクト)なのでジェネレータが有効となります。
その他の例
他にもジェネレータの利用が有効なシーンはたくさんあります。コードを書くと長くなるため、以下に列挙します。
Webサーバへのリクエストのような大量の順次処理が発生するケース
複雑な計算処理を並行で行うケース
形態素解析
データパイプライン処理
再帰処理
ジェネレータを覚えて効率的な実装をしよう
なかなか使うシーンを想像しづらいジェネレータですが、多くのWebアプリケーションフレームワークやライブラリで処理の効率化を考慮して利用されています。
自身で実装するアプリケーションやツールでも、効率的に利用できるケースがあったら積極的に使っていきましょう。
メモリ消費を抑える実装は、アルゴリズムや設計といった基礎的なスキル以外にも、こういった言語の機能でサポートされていることがあるので、ぜひ覚えておきたいところです。