Pythonを学んでいると、「yield」というキーワードに出会って「returnと何が違うの?」と戸惑った経験はありませんか?ジェネレータは最初は分かりにくいですが、仕組みが分かると大量のデータを少ないメモリで扱える、とても便利な道具になります。
この記事では、Pythonのジェネレータ(generator)とyieldの使い方を、初心者の方にも分かるようにコード付きで丁寧に解説します。リストとの違い、ジェネレータ式、実用例まで順番に見ていきましょう。
ジェネレータとは?
ジェネレータとは、「値を1つずつ必要になったときに作って返す」仕組みを持ったイテレータの一種です。リストのようにすべての値を最初にメモリへ並べるのではなく、次の値を求められるたびに計算します。このような性質を遅延評価と呼びます。
関数の中で return の代わりに yield を使うと、その関数はジェネレータ関数になります。
yieldの基本的な使い方
ジェネレータ関数を作ってみよう
1からnまでの数を順に返すジェネレータ関数です。関数の定義方法に不安がある方は、関数(def)の作り方の記事も合わせてご覧ください。
def count_up(n):
i = 1
while i <= n:
yield i
i += 1
for x in count_up(3):
print(x)
# 1
# 2
# 3
print(list(count_up(3))) # [1, 2, 3]yield に到達すると、値を呼び出し元へ返して関数の処理を一時停止します。次の値が必要になると、停止した場所から処理が再開されます。この「途中で止まって、続きから再開できる」点がreturnとの大きな違いです。
next()で1つずつ取り出す
ジェネレータは next() 関数で手動で値を取り出すこともできます。取り出す値がなくなると StopIteration 例外が発生します。for文はこの例外を内部で処理しているため、意識せずに使えます。
gen = count_up(2)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # StopIteration が発生例外の扱いについては例外処理(try-except)の解説記事も参考になります。
リストとジェネレータの違い(メモリ効率)
リストはすべての要素をメモリに保持しますが、ジェネレータは現在の状態だけを保持します。たとえば100万件の数値を扱う場合、リストでは数MBのメモリが必要になるのに対し、ジェネレータは数百バイト程度で済みます。
import sys
big_list = [i for i in range(1_000_000)]
big_gen = (i for i in range(1_000_000))
print(sys.getsizeof(big_list)) # 数MB(環境により異なります)
print(sys.getsizeof(big_gen)) # 100〜200バイト程度数値は環境によって変わりますが、桁違いにジェネレータが小さいことが分かります。
- リスト:全要素を保持する/何度でも読み返せる/インデックスでアクセスできる
- ジェネレータ:1つずつ生成する/メモリ消費が小さい/基本的に1回しか読めない
ジェネレータ式
リスト内包表記の [ ] を ( ) に変えるだけで、ジェネレータ式になります。リスト内包表記についてはリスト内包表記の解説記事をご覧ください。
squares = (x * x for x in range(1, 6))
print(sum(squares)) # 55sum() のように関数の引数に1つだけ渡す場合は、丸括弧を省略して sum(x * x for x in range(1, 6)) と書くこともできます。
なお、ジェネレータは一度使い切ると空になります。同じ結果を何度も使いたいときは、list() に変換して保存しましょう。
実用例:無限に続くフィボナッチ数列
ジェネレータなら「終わりのない数列」も表現できます。リストでは不可能なことが、ジェネレータでは簡単です。
from itertools import islice
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]itertools.islice を使うと、無限ジェネレータから必要な個数だけ取り出せます。
実用例:大きなファイルを1行ずつ処理する
数GBのログファイルを丸ごと読み込むとメモリが足りなくなります。ジェネレータを使えば1行ずつ処理できます。ファイル操作はwith構文の解説記事も参考にしてください。
def read_errors(path):
with open(path, encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
yield line.rstrip()
for line in read_errors("app.log"):
print(line)yield fromで別のジェネレータに委譲する
yield from を使うと、別のイテラブルやジェネレータの値をそのまま順に返せます。
def chain_numbers():
yield from [1, 2]
yield from range(10, 13)
print(list(chain_numbers())) # [1, 2, 10, 11, 12]独学で行き詰まったときは、学習の進め方を見直すのも一つの方法です。Pythonが学べるプログラミングスクールの選び方で比較軸をまとめていますので、参考にしてください。資格取得を目標にするなら、Python3エンジニア認定基礎試験の参考書比較も役立ちます。
よくある間違いと注意点
- ジェネレータを2回ループしても、2回目は何も出力されない(一度使い切るため)
len()やインデックスアクセス(gen[0])は使えない- ジェネレータ関数は呼び出しただけでは中身が実行されない(最初の
next()で動き始める)
もっと効率的にPythonを学びたい人へ
ジェネレータのような概念は、独学だと「なんとなく動くけれど理解できていない」状態になりがちです。マンツーマンで質問しながら学べるスクールなら、つまずきをその場で解消でき、未経験からでも着実にPythonを身につけられます。無料カウンセリングで相談できるスクールもありますので、気になる方はチェックしてみてください。
※本記事には広告(アフィリエイトリンク)を含みます。
Pythonプログラミング|マンツーマンレッスンで初めてをプロに育てる【Python Winner】![]()
まとめ
この記事のポイントを整理します。
- ジェネレータは値を1つずつ生成する仕組みで、メモリ効率が良い
- 関数内で
yieldを使うとジェネレータ関数になり、処理を一時停止・再開できる ( )で書くジェネレータ式なら、リスト内包表記と同じ感覚で書ける- ジェネレータは1回しか読めないので、再利用したいときはリストに変換する
- 大きなファイルや無限数列の処理で特に威力を発揮する
まずは簡単なジェネレータ関数を自分で書いて、next()で動きを確かめてみてください。理解が深まります。
ナノトイラボ 