Pythonで作業メモリを節約できるジェネレータ関数の使い方のイメージ画像

Pythonで作業メモリを節約できるジェネレータ関数の使い方

  • 公開日:2018/11/10
  • 更新日:2018/11/10
  • 投稿者:n bit

Pythonで作業メモリを節約しプログラムの実行処理速度を高速化させるジェネレータ関数について解説。ジェネレータ関数から生成されるジェネレータイテレータはリストオブジェクト等と違いメモリ上に1度に展開しないため膨大な数のものでも処理できます。

  • Python

この記事は約 分で読めます。(文字)

Pythonのジェネレータ関数(generator function)の基本

『ジェネレータ関数(generator function)』は『ジェネレータイテレータ(generator iterator)』と言うオブジェクトを生成するための関数です。『ジェネレータイテレータ』は前回学習した『イテレータ』の中の1つで名前からもなんとなくイメージできますが『ジェネレータ関数から生成されたイテレータ』と理解しておけばよいでしょう。

  • ジェネレータ関数はジェネレータイテレータを生成する関数

ジェネレータ関数やジェネレータイテレータについては前回のイテレータがしっかりと理解できていれば特に難しい事はありません。

イテレータについては下記のページで詳しく解説しています。イテレータについて記憶が曖昧になってしまったりまだ深く理解しきっていない方はもう一度読んでおきましょう。

ジェネレータはメモリ消費量が少ない

ジェネレータ関数を利用する最大のメリットはメモリの消費量が少ないと言うことです。例えば下記のようなリストで渡したテキスト要素を全て小文字化して返してくれる関数の場合を見てみましょう。

words = ['Python', 'CSS', 'HTML', 'JavaScript']


def word_edit(word_list):
new_words = []
for word in word_list:
new_word = word.lower()
new_words.append(new_word)

return new_words

for text in word_edit(words):
print(text)

出力結果

python

css
html
javascript

上記の例ではリスト内の要素が4つしかありませんのでリストに全て格納したものをreturnで返しても大きな問題はありません。しかし、このリスト内の要素の桁数が100万とかあった場合はどうでしょうか。

そのような膨大な数の要素を一度にリストとして生成してしまうと全てメモリ上に配置されてしまいメモリがパンクしてしまう可能性があります。メモリのパンクを防止するためにジェネレータ関数は一度に全ての要素を生成せず必要となったタイミングで要素を1つずつ生成して返すのが特徴です。

ジェネレータ関数を利用した場合の実行処理速度について

ジェネレータ関数を利用する場合、いちどすべての値を格納したリストを作成しメモリ上に配置する処理が不要になります。そのため膨大な量のデータなどを扱うときにはメモリ上にリストが配置されるまでの待ち時間が不要になりトータルの実行処理時間が短縮されるケースが多いです。

下記事例では約1800万行のテキストデータを取得し処理を加えたときのトータル時間をジェネレータ関数を利用しない場合とした場合で比較しています。ループ内の処理時間にはそれほど大きな差はありませんでしたがループ実行前のメモリ上にリストを作成する時間が不要になるため、その分トータルの処理時間を短くすることができました。

データ数方式最終の実行処理時間
約1800万行のデータリストを生成してループで回す1623.2 sec
約1800万行のデータジェネレータからループで回す1026.5 sec

ジェネレータ関数を利用すると必ず実行処理が速くなると言うわけではありませんが、膨大なデータを扱う場合は1度リストにしてしまうよりジェネレータで処理しながら随時値を生成して取り出す方が良いでしょう。

ジェネレータ関数の使い方

簡単なジェネレータ関数を1つ作成してジェネレータイテレータの特徴を実際にコードで確認していきましょう。サンプルコードは以下のようになります。通常の関数との大きな違いは『return』が『yield』に変わっていることです。

def gener1():

yield 'Python'
yield 'CSS'
yield 'HTML'
yield 'JavaScript'

ジェネレータ関数はあくまで関数ですので利用するためには一度インスタンス化する必要があります。ジェネレータ関数をインスタンス化し実態を持ったオブジェクトが『ジェネレータイテレータ』です。

ではジェネレータ関数からジェネレータイテレータを作成し確認してみましょう。

def gener1():

yield 'Python'
yield 'CSS'
yield 'HTML'
yield 'JavaScript'

gener = gener1()

print(type(gener))

出力結果

<class 'generator'>

ジェネレータイテレータをtype関数で確認すると『<class 'generator'>』となっています。イテレータと同じ様な動きをしますがジェネレータ関数から作成したオブジェクトですのでデータ型はイテレータ型ではなくジェネレータ型です。

基本的な動きはイテレータと同じなためnext関数を使うことで1つずつ要素を取り出すことができます。

def gener1():

yield 'Python'
yield 'CSS'
yield 'HTML'
yield 'JavaScript'

gener = gener1()

print(next(gener))
print(next(gener))
print(next(gener))
print(next(gener))

出力結果

python

css
html
javascript

yieldを記述している数だけ要素を取り出せます。

next関数で2つ取り出した後にfor文で回してみるとイテレーションされた位置をきちんと記憶しているため3つめの要素『HTML』からfor文で出力されJavaScript以降は何も表示されてないことが確認できます。

def gener1():

yield 'Python'
yield 'CSS'
yield 'HTML'
yield 'JavaScript'

gener = gener1()

print(next(gener))
print(next(gener))
print('######## for roop start #######')
for word in gener:
print(word)

出力結果

python

css
######## for roop start #######
html
javascript

ジェネレータ関数である『gener1』から生成したジェネレータイテレータ『gener』はイテレータオブジェクトと同じ性質を持ったオブジェクトになっていることが確認できました。

このように『ジェネレータ関数』はイテレータオブジェクトと同じ性質を持つ『ジェネレータイテレータ』を生成することができる関数です。

ジェネレータ関数のサンプルコード

では最初にサンプルで示したリスト内要素の文字列を全て小文字化して返してくれる関数をジェネレータ関数に置き換えていきましょう。

通常のforループでリストを返す関数のサンプルコード

置換前のコードはこのようになっています。

words = ['Python', 'CSS', 'HTML', 'JavaScript']


def word_edit(word_list):
new_words = []
for word in word_list:
new_word = word.lower()
new_words.append(new_word)

return new_words

for text in word_edit(words):
print(text)

出力結果

python

css
html
javascript

ジェネレータ関数で要素を1つずつ返すサンプルコード

ジェネレータ関数に置き換えると下記のようになります。

words = ['Python', 'CSS', 'HTML', 'JavaScript']


def word_edit_gen(word_list):
for word in word_list:
yield word.lower()

for text in word_edit_gen(words):
print(text)

出力結果

python

css
html
javascript

コードの記述がとても簡素になり可読性が良くなりましたね。

大きな違いはリストへの格納が必要がなくなったため空リストの作成行が不要になったのと、

new_words = []

appendメソッド呼び出し処理も必要なくなりました。

new_words.append(new_word)

関数の最終行では『return』が『yield』に置き換わっています。

        new_word = word.lower()






return new_words

↓:下記に変更

yield word.lower()

通常の関数とジェネレータ関数の違いはこの『return』が『yield』に変わるところです。『yield』がついた時点でジェネレータ関数になったと言えるでしょう。

それではこのジェネレータ関数をインスタンス化して作成したジェネレータイテレータの動きをnext関数等を使って見てみましょう。

words = ['Python', 'CSS', 'HTML', 'JavaScript']


def word_edit_gen(word_list):
for word in word_list:
yield word.lower()

print(next(words_gen))
print('test1')
print(next(words_gen))
print('test2')

print('######## gener roop start #######')
for text in word_edit_gen(words):
print(text)

出力結果

python

test1
css
test2
print('######## gener roop start #######')
html
javascript

正しくイテレータと同じ動きをしています。

これで『イテレーションした状態を記憶』しており、『呼び出されるたびに1つずつ要素を生成して返す』ことが出来る『ジェネレータイテレータ』を生成する『ジェネレータ関数』が作成できました。膨大な数の要素を扱ってもメモリがパンクすることなく実行処理を行えます。

今日のdot

今回for文からリストを生成して返す関数をジェネレータ関数に置き換えながらジェネレータ関数について学習しました。基本的に今回のようなfor文からリストを生成するタイプの関数であれば、ほぼジェネレータ関数に置き換えることができます。

関数から受け取ったリストをその後再度for文で回して1つずつ処理を行うような場合であればジェネレータ関数の出番です。ジェネレータ関数で作成しておくことでメモリの消費量を抑えることができ大量のデータ扱っても処理できます。