Pythonのジェネレータを試してみる|ジェネレータ

python_generator_pc Python
python_generator_pc
この記事は約8分で読めます。

はじめに

ジェネレータはPythonでも少し高度な機能かと思いますが、for文に似ているかと思います。

for文の場合、指定した回数を一気に処理してしまいますが、ジェネレータの場合は呼び出したときに1回目の処理(最初のyield)を実行してから呼び出し元に処理を戻し、次また呼び出された際には前回どこまで処理したかを記憶しているので続きから実行する、という流れになります。

イメージ的にはfor文の場合は処理を一気に終わらせますが、ジェネレータの場合は小出しに処理すると考えればわかりやすいかもしれません。

また、ジェネレータはPython特有の機能だそうですが、JavaScriptやC#などにもあるそうです。

そちらで経験のある方はPythonのジェネレータはすんなり頭に入るのかなと思います。

でも、やっぱりジェネレータってちょっと難しいですよね。

ジェネレータの基本的な使い方(サンプル)

ジェネレータを理解するには、まずは簡単なものからいきましょう。

とその前に、for文の処理を一度確認してみたいと思います。

例えばfor文の場合、次のようにループ処理を一気に行ってしまいます。

fruits = ['リンゴ', 'バナナ', 'メロン']
for i in fruits:
    print(i)

結果は以下の通りです。

リンゴ
バナナ
メロン

例えば、1回目のループ処理後に何か別の処理を入れたいと思っても、for文やwhile文の場合難しいですよね。

ですが、ジェネレータを使うとその処理が可能になります。

def test_gen():
    yield '1回目の処理です。'
    print('11111')
    yield '2回目の処理です。'
    yield '3回目の処理です。'

g = test_gen()
# 1回目の処理
print(next(g))

こちらの結果は以下になります。

1回目の処理です。

ふむふむ。

next(g)の処理でジェネレータ関数内の1個目のyield行が実行されていますね。

でわ、次の場合はどうなるでしょうか。

def test_gen():
    yield '1回目の処理です。'
    print('11111')
    yield '2回目の処理です。'
    yield '3回目の処理です。'

g = test_gen()
# 1回目の処理
print(next(g))
# 2回目の処理
print(next(g))

next(g)を2回実行した結果がこちらです!

1回目の処理です。
11111
2回目の処理です。

1回目のnext(g)の処理では、1個目のyield行で処理を実行した後呼び出し元に戻りジェネレータ関数内での処理は一時中断されます。

2回目のnext(g)の処理では、前回の続きであるprint(‘11111’)の処理から始まり次のyield行の「yield ‘2回目の処理です。’」を実行してから呼び出し元に処理を戻し、ジェネレータ関数内の処理を一時中断します。

以降は同じですね。

なんとなくイメージはつきましたでしょうか?

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

ジェネレータを使うメリットとしては、コード量の削減やメモリ効率が良くパフォーマンスの向上につながる点だそうです。

確かにスマートにそしてコンパクトにコーディングできるので、コード量の削減や可読性は上がりそうですね。

ループで処理する際は最後のループを抜けるとStopIterationが送られるので、例えば例外処理で何かしらの処理を入れてあげたいのであれば、捕捉してあげればよいかと思います。

ジェネレータではなく単純にリストやタプルを使えばいいじゃんと思われるかもしれませんが、要素数が非常に多い場合などリストやタプルだとメモリ領域を圧迫してしまう恐れがあります。

ジェネレータであれば処理する際に要素を取り出しますので、メモリ効率が良いということですね。

機械学習や画像処理など、データ解析する際にはジェネレータの出番になってくると言われます。

扱うデータ量が半端ないですし。

私はまだそこまで行けていませんので、ジェネレータのありがたみがまだまだ微妙なところです。。

ジェネレータ関数の戻り値は?

ジェネレータ関数の戻り値は、type()で確認すればわかりますが「<class ‘generator’>」(ジェネレータイテレータ)です。

yield式の行で処理結果を呼び出し元に返した後、処理を一時中断しこの状態を保持します。

next()などで再度呼び出されたら、中断していたyield行の次の行から処理をスタートさせます。

上記サンプルコードで実行したやつですね。

while文を使ったジェネレータ

ジェネレータ関数内でwhile文を使ったサンプルがこちらですが、引数で受け取った数文の処理をする内容です。

next()で呼び出される度にyield行まで実行し、呼び出し元に処理を戻す流れです。

def g_func(n):
    while n:
        print(f'while処理内です。{n}')
        yield n
        n -= 1

g = g_func(3)
# 1回目のnext(g)
print(next(g))

結果は以下になります。

while処理内です。3
3

next()を1回だけ実行していますが、呼び出された側のジェネレータ関数はwhile文があるにも関わらず、yield行のところで処理を呼び出し元に返しています。

本来のwhile文のように一気に処理していませんね。

でわ、next()を2回実行してみます。

def g_func(n):
    while n:
        print(f'while処理内です。{n}')
        yield n
        n -= 1

g = g_func(3)
# 1回目のnext(g)
print(next(g))
# 2回目のnext(g)
print(next(g))

結果はこんな感じですね。

while処理内です。3
3
while処理内です。2
2

1回処理しては呼び出し元に処理を戻してジェネレータ関数内の処理は一時中断、ってな感じです。

ジェネレータ内包表記

リスト内包表記の場合は以下のように「[ ]」を付けてコーディングしていましたが、ジェネレータ内包表記の場合は「()」を付けるので注意が必要です。

リスト内包表記の場合は以下の通りですね。

l = [1, 2, 3, 4, 5]
mylist = [i**2 for i in l]
print(mylist)

結果はこちらになります。

[1, 4, 9, 16, 25]

単純に2乗しているだけです。

ただ、ジェネレータ内包表記の場合は上記でも触れましたが、「[ ]」ではなく「()」を使います。

パッと見、タプルと勘違いしそうですが、type()で確認するとちゃんとジェネレータになっています。

gen = (i ** 2 for i in range(5))
print(type(gen))

print(type(gen))
print(type(gen))
print(type(gen))
print(type(gen))
print(type(gen))

結果はこちらです。

<class 'generator'>
0
1
4
9
16

ちゃんとジェネレータになっていますし、それぞれ2乗した結果が表示されています。

ジェネレータの注意点

ジェネレータ関数では、returnが使えません。

yieldがあるからですね。

ジェネレータをlen()で読み込もうとするとエラーになります。

自分で書いたコード以外にもimportしてあるモジュール内でlen()が使われていた場合も同様です。

その際はジェネレータをリスト変換した後にlen()で読み込む方法をとります。

ただし、無限で処理を繰り返すジェネレータなどをlen()で読み込むとメモリ領域を圧迫し危険です。

まとめ

ジェネレータはyieldが入っているかで見分けはつきやすいですが、内包表記にする場合はリスト内包表記と違って「()」にしないといけなかったり、注意が必要な箇所が多いと思います。

len()でジェネレータを読み込もうとすると「TypeError: object of type ‘generator’ has no len()」ってエラーになりますし、使い慣れるまで少し時間がかかると思います。

もっともっとコードを書いていって、スマートに使いこなせるようになりたいです。

コメント

タイトルとURLをコピーしました