Python PR

【Python】ジェネレータの定義と使い方を解説

記事内に商品プロモーションを含む場合があります

この記事では、Pythonのジェネレータの使い方を解説します。

ジェネレータとは、イテレータを簡単に作成するための機能です。リストやタプルなどを使わなくても複数の値を返す関数を定義することができます。

イテレータについては、以下の記事を参考にしてください。

Linkイテラブルとイテレータについて

それでは、ジェネレータの使い方を見ていきましょう!

ジェネレータ関数の定義

ジェネレータ関数は、関数内でyield文を使って値を返します。

def 関数名():
    yield 戻り値

yieldは、同時にいくつでも定義することもできます。

def 関数名():
    yield 戻り値1
    yield 戻り値2
    yield 戻り値3
  • yield文が処理されても関数は終了しない

例として、簡単なジェネレータ関数を定義して呼び出してみる。その際、生成したジェネレータとオブジェクトの型を出力する。 :

# ジェネレータ関数
def func():
    yield 1
    yield 'Hello'
    yield [1, 2, 3]

result = func()
print(result, type(result))
# <generator object func at 0x10e904a00> <class 'generator'>

このように、ジェネレータ関数はジェネレータオブジェクトを返します。

ジェネレータオブジェクト

ジェネレータ関数を実行すると、以下のような「ジェネレータオブジェクト(ジェネレータイテレータ)」が返されます。

<generator object 関数名 at 0x10bef73d0>

ジェネレータオブジェクトは、for文で各要素にアクセスしたり、

for val in ジェネレータオブジェクト:
    print(val)

リストやタプルに変換することができます。

list(ジェネレータオブジェクト)

例として、渡された文字列を1文字ずつ文字コードにして返すジェネレータ関数から生成したジェネレータオブジェクトをfor文で実行してみたり、リストに変換してみる :

def ords(string: str):
    for char in string:
        # yieldで文字コードを返す
        yield ord(char)


for o in ords('abc'):
    print(o)

# リストに変換
print(list(ords('abc')))

実行結果

97
98
99
[97, 98, 99]

ジェネレータの仕組み

ジェネレータは、以下のように処理されています。

  1. yield文で値を返した時に処理を中断し、その場所を記憶する
  2. 新たに値が要求されたら記憶した場所から処理を再開する

上記のような処理を繰り返すことでジェネレータは複数の値を返しています。また、値が要求されるたびに演算して要素を返すため処理や使用するメモリを分散することができ、効率が良いとされています。

例えば以下のコードを見てください。next()で要素を要求されるたび、関数内のprint()によって値が出力されているのがわかります。

def generator():
    print('start')
    print('yield 1')
    yield 1
    print('yield 2')
    yield 2
    print('yield 3')
    yield 3
    print('end')


g = generator()

# next()で次の要素を要求できる
print('next 1')
print(next(g))
print('next 2')
print(next(g))
print('next 3')
print(next(g))
print('next 4')
print(next(g))

実行結果

next 1
start
yield 1
1
next 2
yield 2
2
next 3
yield 3
3
next 4
end
Traceback (most recent call last):
  File "main.py", line 22, in <module>
    print(next(g))
StopIteration

このように、ジェネレータは値を要求されるたびに演算して要素を返しています。処理が膨大な場合にはジェネレータを使うことでコストを分散させることができます。

ジェネレータに値を渡す

ジェネレータに値を渡すにはジェネレータオブジェクトからsend(渡す値)を実行します。例えば以下のような感じです。

# 適当なジェネレータオブジェクトの生成
g = generator()

next(g)
# ジェネレータに 5 を渡す
g.send(5)
ポイント
  • send()が実行されるとジェネレータのコードが実行を再開する
  • 値が渡される場所は前回の実行で値を返したyield
  • なので、最初からsend()を実行することはできない

ジェネレータ内で受け取るには以下のよう記述します。(1つのyield文で二役こなします。混乱しやすいので注意してください。)

変数 = (yield 戻り値)

それでは簡単な例を見てみましょう:

def gen():
    # まず最初の next() で 1 を返し処理が中断される
    # 次に処理が再開される際に send() で値が渡された場合、n にその値が渡される
    n = (yield 1)
    # 受け取った n を返し処理が中断される
    # 次に処理が再開される際に send() で値が渡された場合、v にその値が渡される
    v = (yield n)
    # v を返す
    yield v
    

g = gen()

print(f"1: {next(g)}")      # 1: 1
print(f"2: {g.send(5)}")    # 2: 5
print(f"3: {g.send(10)}")   # 3: 10

send()で値が渡されなかった場合は変数にNoneが代入されるので、send()以外で実行を再開する可能性があるときは必ず値をチェックするようにしてください。

ジェネレータを終了する: return

ジェネレータ内でreturnを実行することでreturnで返した値を持ったStopIteration例外を発生させ、ジェネレータを終了させることができます。

def gen():
    yield 1
    # send() で値を受け取る
    val = yield 2
    # 値が渡されなかったり、負の数が渡された場合
    if val is None or val < 0:
        # StopIteration(-1) を発生させる
        return -1
    yield val
    

g = gen()

print(f"1: {next(g)}")      # 1: 1
print(f"2: {next(g)}")      # 2: 2
# 負の数を渡してみる
print(f"3: {g.send(-5)}")   # StopIteration: -1

returnが実行された場合、関数の終わりに到達したのと同じように値の生成が終了してジェネレーターがそれ以上の値を返しません。

ジェネレータ式

単純なジェネレータならば、さらに簡潔に式として記述することができます。ジェネレータ式は、内包表記と同じ書き方をします。[]ではなく()を使う点に注意してください

(戻り値 for 変数名 in イテラブルオブジェクト)

先ほどのサンプルで作成したジェネレータ関数をジェネレータ式で書き換えてみます :

元のコード

def ords(string: str):
    for char in string:
        # yieldで文字コードを返す
        yield ord(char)


for o in ords('abc'):
    print(o)

ジェネレータ式で書き換えたコード

for n in (ord(char) for char in 'abc'):
    print(n)

実行結果

97
98
99

for文と合わせて2行で実装できてしまいました。このように単純なジェネレータはジェネレータ式を使うことで簡単に実装できます。

ジェネレータオブジェクトは使い回せない

ジェネレータオブジェクトが呼び出された場合、記憶した場所から処理を再開します。例えば、以下のような場合も同様です。

def func():
    yield 1
    yield 2
    yield 3

g = func()

print('1度目のループ')
for v in g:
    print(v)

print('2度目のループ')
for v in g:
    print(v)

実行結果

1度目のループ
1
2
3
2度目のループ

1度目のループで最後の要素まで処理したので、2度目のループでは何も出力されません。何度も参照したい場合は、リストやタプルに変換して使いましょう!

まとめ

この記事では、Pythonのジェネレータの使い方を解説しました。

ジェネレータを使う機会は多くないですが、複数の値を返したいけどリストやタプルなどを使いたくない場合は実装を検討してみてはいかがでしょうか?

一度で処理するにはコストがかかり過ぎる場合にもジェネレータを使うことを考慮しましょう!

今回のおさらい

簡単に今回のコードをおさらいしておきましょう!

# ジェネレータ
def 関数名():
    yield 戻り値1
    yield 戻り値2
    yield 戻り値3

# ジェネレータ式
(戻り値 for 変数名 in イテラブルオブジェクト)

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ