Pythonのジェネレータを理解する
これはなに
Pythonのジェネレータを理解するための記事。
ジェネレータとは
Pythonにおけるジェネレータとは、「次のデータが必要になったら、そのときにデータをつくる仕組み」である。ジェネレータが呼び出されると、関数が実行されて値を返し、関数は一時停止する。次に呼び出されると、一時停止した箇所から再開される。
比喩を使うと、ジェネレータとは、頭から1ページずつ本を読むようなものである。
ふつう、本というものは最初から最後まですべてのページが手元にある。そのため、好きなときに好きなページを開けるし、好きなページを何度も読める。しかし、本が非常に分厚いと、置く場所がなくなるし、持ち運ぶのも大変である。
ジェネレータの場合は、読みたいときだけ次のページを作り出す。ゆえに、手元には1ページしかない。そのため、置く場所を取らないし、持ち運びも楽である。しかし、途中から読み始められないし、同じページを何度も読むことはできない。
もう少しプログラム的な話をする。たとえば、1から10までの数値を順番に得るプログラムを考える。リストを使うと、1から10までの数値をすべて作成することになる。
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# この時点で1から10までの数値がすべてメモリに格納される
# すでにメモリに格納された値を取り出すだけ
print(numbers[0]) # 1
print(numbers[1]) # 2
print(numbers[2]) # 3
しかし、ジェネレータを使うと、1から10までの数値を必要なときに1つずつ作成できる。
def generate_numbers():
for i in range(1, 11):
yield i
numbers = generate_numbers()
# この時点では1から10までの数値はまだ生成されていない
# `next`で呼ばれるたびに1つずつ数値を生成する
print(next(numbers)) # 1
print(next(numbers)) # 2
print(next(numbers)) # 3
もっと真面目な話をすると、Pythonにおけるジェネレータとは、遅延評価の性質を持つ特別なイテレータの一種である。イテレータの一種なので、__iter__()
と__next__()
の2つのメソッドを持つ。また、遅延評価の性質を持つため、__next__()
メソッドが呼ばれるまで次の要素を生成しない。__next__()
メソッドが呼ばれてはじめて次の要素を生成する。
ジェネレータの利点
ジェネレータを利用すると、すべての値をメモリに格納する必要がない。そのため、メモリの使用量を抑えられる。これは、とくに大量のデータを扱う場合に有用である。たとえば、ファイルの内容を1行ずつ読み込む場合、ジェネレータを使うと、ファイル全体をメモリに読み込む必要がなくなる。
def read_file(file_path):
with open(file_path) as f:
for line in f:
yield line
また、すべての値をメモリに格納する必要がないため、無限のデータを扱える。たとえば、フィボナッチ数列をジェネレータで表現できる。
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3
ジェネレータを利用するシーン
ジェネレータは、次のような場面で利用すると便利である。
- 大規模なデータを扱う場合
- 無限のデータを扱う場合
- メモリを節約したい場合
- 計算量の大きいシーケンスを扱う場合
とくに、大規模なリストを返す関数では、リストの代わりにジェネレータを使うことを検討すると良い。たとえば、次のような関数は、リストを返すのではなく、ジェネレータを返すようにすると良い。
def generate_numbers():
for i in range(1000000):
yield i
また、大量の入力に対してリスト内包表記を使うと、メモリを大量に消費する可能性がある。その場合、ジェネレータ式を使うとメモリを節約できる。
numbers = (i for i in range(1000000))
ジェネレータの欠点
ジェネレータはステートフルなオブジェクトである。次の呼び出しあるいは関数の終了まで関数の実行は保持され、一度next()
で呼び出した値は破棄される。そのため、一度値を生成した後だと、再度同じ値を取得したり、それより前に生成した値を取得したりできない。
ジェネレータの作り方
ジェネレータを作る方法は2つある。ひとつは、ジェネレータ関数を使う方法。もうひとつは、ジェネレータ式を使う方法である。
ジェネレータ関数
ジェネレータ関数とは、yield
文を使って値を返す関数である。通常の関数と同様にdef
で定義するが、return
の代わりにyield
を使用する。yield
文を使うことで、関数の実行を一時停止して値を返せる。
def generate_numbers():
for i in range(1, 11):
yield i
ジェネレータ式
ジェネレータ式とは、リスト内包表記と似た構文で、ジェネレータを生成する方法である。リスト内包表記との違いは、[]
ではなく()
を使う点である。基本的な形は以下のようになる。
generator = (expression for item in iterable)
たとえば、1から10までの数値をジェネレータ式で生成する場合は、次のようになる。
numbers = (i for i in range(1, 11))
ジェネレータ式に条件を設けることもできる。たとえば、上記のジェネレータで偶数だけを取り出す場合は、次のようになる。
numbers = (i for i in range(1, 11) if i % 2 == 0)
まとめ
大規模なデータをリストに格納するぐらいなら、ジェネレータを用いたほうがメモリを節約できる。
Discussion