Pythonのジェネレータ

記事の内容

 この記事では、Pythonのジェネレータ(generator)とは何か、ジェネレータの作り方と使い方、どんな場面で利用するものなのか、について説明していきます。
 ジェネレータは、巨大なデータを処理する際、メモリを浪費しない目的でよく利用されます

***わからない用語があるときは索引ページへ***

1.ジェネレータの作成と呼び出し

 ジェネレータは、関数とよく似た方法で作成します。関数がdef宣言で定義されるのと同様に、ジェネレータもdef宣言で定義されます。ただし、値の返し方が違っています。
 関数が処理結果を呼び出し元に返すときは、return文を使って、

return <呼び出し元に返すオブジェクト>

の記述で値を返しますが、ジェネレータはreturn文の代わりにyield文を使って、

yield <呼び出し元に返すオブジェクト>

の形式で処理結果を返します。
 関数とジェネレータは、何が違うのでしょうか。まずはサンプルプログラムで動きを見て、関数とジェネレータの違いをみてみましょう。

##generator_ex1.py

##ジェネレータgen_numの作成
##引数に初期値start=0, step=1を設定
def gen_num(start=0, step=1):
   i = start
   
   ##無限ループ
   while True:
       yield i
       i += step

##プログラム実行開始位置
for i in gen_num():
   print("i =", i)
   
   ##iの値が10以上になったら、ループを抜ける
   if i >= 10:
       break

 ジェネレータ"gen_num"を見てみると、yeildがdefのプログラムブロックの途中にありますね。
 関数は、呼び出されるたびにdefのプログラムブロックの頭から実行されます。これに対してジェネレータは、yeildで値を返すと、次に呼び出されたときに、続きの位置から処理を再開します
 実行結果をみてみましょう。

i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10

ジェネレータgen_num()は引数startに初期値0がセットされているので、最初に

yield i

を実行するときは0を返します。
 "generator_ex1.py"では、for文でgen_num()を呼び出しています。次に呼び出されたときには、最初に実行されたyield文の続きから処理が再開され、

i += step

が実行されます。ここでiにstep(=1)が加算され、次にyeild文を実行するときは2を返します。”generator_ex1.py"では、for文がi>=10となってbreakされるまでこれを繰り返し実行しています。
 ジェネレータは"generator_ex1.py"のように、for文でよく利用されます。

2.ジェネレータを使うメリット

 for文ではリストやタプルなどのイテラブルなオブジェクトを使った方がプログラムはわかりやすいですよね。なぜわざわざジェネレータを使うのでしょうか。
 データ分析のプログラムでは、膨大な回数のループを回したくなることがあります。このようなときにイテラブルなオブジェクトでループを回そうとすると、それだけで大量にメモリを消費してしまいます。取り出される要素は繰り返し処理の1回しか必要ないのに、メモリだけは大量に消費してしまうということです。
 ジェネレータを使うと、ループで使う次のデータを都度、計算して返してくれます。ループ処理の過去、未来の膨大なデータをメモリで記憶してく必要がなくなるので、プログラムによっては、ジェネレータを使うとメモリの消費を劇的に抑えることができるのです。
 また、巨大なデータの解析、例えばサイトの字句解析などを行う場合、特定パートの解析が終わればその部分のデータは不要となるのに、最初に全データを読み込んでから解析をすると、メモリを浪費します。このような場面でもジェネレータが活用されます。

3.ジェネレータの戻り値を1つずつ取り出す

 "generator_ex1.py"では、for文を使ったジェネレータの使い方を説明しました。次のサンプルプログラムでは、ジェネレータを呼び出して、戻り値を1つずつ取り出す方法を説明します。
 ジェネレータは、作り方は関数とそっくりですが、関数とは違う動きをします。このため、呼び出し方やデータの取得の仕方も関数とは違うのです。
 "generator_ex1.py"で作成したgen_num()ジェネレータを使って説明します。

##generator_ex2.py

##ジェネレータの作成
##初期値i=0を設定
def gen_num(start=0, step=1):
   i = start
   
   ##無限ループ
   while True:
       yield i
       i += step


##プログラム実行開始位置
##最初にge_numの、インスタンスを生成する。
gen_num_obj = gen_num()

##next関数に生成したインスタンスを引数として渡すと
##gen_num()の次の戻り値を取得できる
print("i =", next(gen_num_obj))
print("i =", next(gen_num_obj))
print("i =", next(gen_num_obj))
print("i =", next(gen_num_obj))
print("i =", next(gen_num_obj))

実行結果

i = 0
i = 1
i = 2
i = 3
i = 4

以下、サンプルプログラムの説明です。
ジェネレータを呼び出す場合は、まずはじめに

<変数名> = <ジェネレータ名>()

の形式で、<変数名>の名前のインスタンスを生成します(インスタンスについては、オブジェクトの説明記事で詳しく説明します)。
 "generator_ex1.py"では、

gen_num_obj = gen_num()

でgen_num_objという変数名(インスタンス名)のインスタンスを生成しています。
 次に、ここで生成したインスタンスを、next関数の引数として渡すことにより、値を1つずつ順番に取り出していくことができます。

print("i =", next(gen_num_obj))
print("i =", next(gen_num_obj))
    :
    :

4.ジェネレータのインスタンスについて

 インスタンスは、プログラムの中でいくつでも生成できます。つまり、プログラムの中で同じジェネレータを使った別々の並びをいくつでも利用できるようになっています。
 次のサンプルプログラムで動きを見てみましょう。

##generator_ex3.py

##ジェネレータの作成
##初期値i=0を設定
def gen_num(start=0, step=1):
   i = start
   
   ##無限ループ
   while True:
       yield i
       i += step


##プログラム実行開始位置
##最初にge_numの、インスタンスを2つ生成する。
gen_num_obj1 = gen_num()
gen_num_obj2 = gen_num()

##gen_num_obj1のインスタンスを出力
print("obj1 i =", next(gen_num_obj1))
print("obj1 i =", next(gen_num_obj1))
print("obj1 i =", next(gen_num_obj1))

print("\n")

##gen_num_obj2のインスタンスを出力
print("obj2 i =", next(gen_num_obj2))
print("obj2 i =", next(gen_num_obj2))
print("obj2 i =", next(gen_num_obj2))

実行結果

obj1 i = 0
obj1 i = 1
obj1 i = 2

obj2 i = 0
obj2 i = 1
obj2 i = 2

サンプルプログラムのインスタンス、gen_nub_obj1とgen_num_obj2はどちらもジェネレータgen_num()のインスタンスですが、それぞれのインスタンスは独立していて、別々にジェネレータの並びを動かしていることがわかると思います
 つまり、ジェネレータの利用にあたっては、まずインスタンスを生成し、そのインスタンスを使ってデータを生成してきます。この使い方は、後の記事で説明するクラスオブジェクトの使い方とよく似ています。

前の記事 次の記事

 




この記事が気に入ったらサポートをしてみませんか?