ジェネレータ(generator)は、値を1つずつ、必要になったときに生成する仕組みです。関数の中でreturnの代わりにyieldを使うと、その関数はジェネレータになります。リストのように全部の値を一度にメモリへ用意するのではなく、使うぶんだけ順に作るため、巨大なデータや無限に続く数列でもメモリを圧迫しません。
つまずきやすいのは、ジェネレータは一度回すと使い切ってしまい、2回目は空になること、そして呼び出しただけでは中の処理が実行されない(遅延評価)ことです。この2つの性質を知らないと、「データが消えた」「処理が動かない」と戸惑います。この記事では、実機のPythonで確認しながら、ジェネレータとyieldを整理します。
- 関数の中で
yieldを使うとジェネレータになります。値を1つずつ返します。 - 値は
next()で1つずつ、またはforでまとめて取り出します。 - 呼び出しただけでは本体は動きません(遅延評価)。取り出すときに初めて実行されます。
- ジェネレータ式
(x for x in ...)はリスト内包[...]よりメモリを使いません。 - 一度回すと使い切ります。2回目のループは空になります。
- 巨大データ・無限数列・1行ずつの処理など、メモリを節約したい場面で活躍します。
一気に作るリスト内包表記との違い、forでの反復に関わるenumerate/zip、土台となる関数(def)もあわせて参考になります。
ジェネレータとは(yieldで作る)
関数の中でreturnではなくyieldを使うと、その関数はジェネレータ関数になります。呼び出すと、値を順に生み出す「ジェネレータオブジェクト」が返ります。
def count_up(n):
for i in range(1, n + 1):
yield i # return ではなく yield
g = count_up(3)
print(type(g)) # <class 'generator'>
# yield は「ここで値を1つ返して、いったん止まる」
# 次に呼ばれると、続きから再開する
実機でも、count_up(3)の戻り値の型はgeneratorでした。yield iは「ここで値iを1つ返して、いったん処理を止める」という意味です。普通の関数のreturnは1回で終わりますが、yieldは値を返したあとも状態を保ったまま止まり、次に呼ばれると続きから再開します。これが「1つずつ生成する」動きの正体です。
next と for で取り出す
ジェネレータから値を取り出すには、next()で1つずつ取るか、forでまとめて取り出します。最後まで取り切ったあとにnext()を呼ぶとStopIterationになります。
def count_up(n):
for i in range(1, n + 1):
yield i
# next() で1つずつ
g = count_up(3)
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
# print(next(g)) # StopIteration(もう値が無い)
# for でまとめて(StopIteration は自動で処理される)
for x in count_up(3):
print(x) # 1, 2, 3
実機でも、next(g)を呼ぶたびに1、2、3と順に返り、4回目でStopIterationになりました。普段はforでループするのが基本で、forはStopIterationを自動的に処理して終了してくれます。next()を直接使うのは、1つずつ手動で進めたい特別な場面です。
遅延評価:呼んだだけでは動かない
ジェネレータの大きな特徴が遅延評価(lazy evaluation)です。ジェネレータ関数を呼び出しても、その時点では中の処理は実行されません。値を取り出そうとした瞬間に、初めて必要なぶんだけ実行されます。
def gen():
print("本体が実行された")
yield 1
g = gen()
print("呼び出し済み(まだ何も表示されない)")
# next() で初めて本体が動く
next(g)
# → ここで「本体が実行された」が表示される
実機で確認したところ、gen()を呼び出した時点では中のprint("本体が実行された")は実行されず、next(g)で値を取り出そうとした瞬間に初めて実行されました。これが遅延評価です。ジェネレータは「値の作り方(レシピ)」を用意するだけで、実際に作るのは取り出すときなのです。この性質のおかげで、無限に続く数列や、まだ存在しない(これから計算する)値も扱えます。逆に、「ジェネレータを作ったのに処理が動かない」と感じたら、それは取り出していないだけ、ということがよくあります。list()で囲むかforで回すと、全部の値が取り出されて処理が実行されます。
メモリ効率:ジェネレータ式 vs リスト内包
ジェネレータは()で囲むジェネレータ式でも作れます。リスト内包表記の[]を()に変えるだけです。この2つはメモリの使い方が大きく違います。
import sys # リスト内包: 全要素をメモリに作る lst = [x * x for x in range(10000)] # ジェネレータ式: () で囲む。値は取り出すときに作る gen = (x * x for x in range(10000)) print(sys.getsizeof(lst)) # 約 85000 バイト(全部を保持) print(sys.getsizeof(gen)) # 約 200 バイト(中身は持たない)
実機で確認したところ、1万個の二乗をリスト内包[...]で作ると約85,000バイト、ジェネレータ式(...)では約200バイトと、メモリ使用量に圧倒的な差が出ました。リスト内包は全要素をメモリに展開しますが、ジェネレータ式は「作り方」だけを持ち、値は取り出すたびに1つずつ計算するためです。合計を求める・1件ずつ処理するなど、全要素を同時に保持する必要がない場面では、(...)のジェネレータ式を使うとメモリを大幅に節約できます。たとえばsum(x * x for x in range(10000))のように、関数に直接渡すときはかっこも省略でき、巨大なデータでもメモリを圧迫しません。逆に、要素に何度もアクセスする・インデックスで取り出す場合はリストが向いています。
一度きりしか回せない
注意したいのが、ジェネレータは一度取り出すと使い切ってしまうことです。同じジェネレータを2回ループしようとすると、2回目は何も残っていません。
def count_up(n):
for i in range(1, n + 1):
yield i
g = count_up(3)
print(list(g)) # [1, 2, 3](ここで全部取り出した)
print(list(g)) # [](もう空。2回目は何も無い)
# もう一度使いたいなら、作り直す
g = count_up(3)
print(list(g)) # [1, 2, 3]
実機で確認したところ、list(g)で1回目は[1, 2, 3]が得られましたが、同じgに対する2回目のlist(g)は[](空)になりました。ジェネレータは取り出した値を覚えておかず、進んだら戻れないためです。「ループしたあと、もう一度ループしたら何も出てこない」というのは、この使い切りが原因です。同じ値を複数回使いたいなら、ジェネレータを作り直すか、最初にdata = list(ジェネレータ)でリストに変換して保持しておきます。enumerate や zip、mapなども同じく一度きりの性質を持つので、あわせて覚えておくとよいです。
yield from・無限ジェネレータ
yield fromを使うと、別のリストやジェネレータの値をまとめてyieldできます。また、while Trueと組み合わせると、無限に続くジェネレータも作れます(遅延評価なので、必要なぶんだけ取り出せます)。
# yield from: 別のリスト/ジェネレータの値を順に流す
def chain():
yield from [1, 2]
yield from [3, 4]
print(list(chain())) # [1, 2, 3, 4]
# 無限ジェネレータ(必要な分だけ取り出す)
def infinite():
i = 0
while True:
yield i
i += 1
inf = infinite()
print([next(inf) for _ in range(5)]) # [0, 1, 2, 3, 4]
実機でも、yield fromで2つのリストをつなげて[1, 2, 3, 4]、無限ジェネレータから5個だけ取り出して[0, 1, 2, 3, 4]が得られました。無限ループでも、遅延評価のおかげで全部を作ろうとはせず、取り出したぶんだけ生成されます。だからこそ「終わりのない数列」も安全に扱えます。ただし無限ジェネレータをlist()やforで無条件に回すと止まらなくなるため、next()で必要な回数だけ取るか、条件でbreakするようにします。
主なポイントまとめ
ジェネレータの要点をまとめます。
| 項目 | ポイント |
|---|---|
| 作り方 | 関数内でyield、または(式 for ...) |
| 取り出し | next()で1つ、forでまとめて |
| 遅延評価 | 取り出すまで本体は実行されない |
| メモリ | 全要素を保持しない(巨大データに強い) |
| 使い切り | 一度回すと空になる(再利用は作り直す) |
| 連結 / 無限 | yield from / while True |
よくある失敗
同じジェネレータを2回ループする
2回目は空です。再利用するなら作り直すか、list()でリストにして保持します。
呼んだだけで処理が動くと思う
遅延評価です。forやlist()で取り出して初めて実行されます。
巨大データをリスト内包で作る
メモリを大量に使います。全保持が不要ならジェネレータ式(...)を使います。
無限ジェネレータをlistで回す
止まらなくなります。next()で回数を決めるかbreakします。
nextを最後まで使い切ってStopIteration
値が無くなるとStopIterationです。通常はforで回せば自動処理されます。
よくある質問
returnの代わりにyieldを使うとジェネレータになります。リストのように全要素を一度にメモリへ用意しないため、巨大なデータや無限の数列をメモリを節約しながら扱えます。returnは値を返して関数を終了しますが、yieldは値を1つ返したあと状態を保ったまま止まり、次に呼ばれると続きから再開します。これにより、値を1つずつ順に生成できます。yieldを含む関数は自動的にジェネレータになります。data = list(ジェネレータ)でリストに変換して保持するか、ジェネレータを作り直してください。enumerateやzipなども同じ一度きりの性質を持ちます。(...)が向いています。要素に何度もアクセスしたり、インデックスで取り出したりするならリスト内包[...]を使います。巨大なデータではこの差が大きく効いてきます。forでループするか、list()で囲んで値を取り出すと、初めて処理が実行されます。「動かない」のではなく「まだ取り出していない」だけ、というケースがほとんどです。まとめ
- 関数内で
yieldを使うとジェネレータ。値を1つずつ生成します。 - 取り出しは
next()かfor。取り出すまで本体は動きません(遅延評価)。 - ジェネレータ式
(...)はメモリを節約。巨大データに強いです。 - 一度回すと使い切り。再利用は作り直すか
list()で保持します。 yield fromで連結、while Trueで無限ジェネレータが作れます。
ジェネレータは、メモリを節約しながら大量データや無限の数列を扱える、Pythonの強力な機能です。「取り出すまで動かない(遅延評価)」「一度きりで使い切る」という2つの性質さえ押さえれば、ログの1行ずつ処理や巨大ファイルの読み込みなど、実用的な場面で安全に活用できます。まずはリスト内包の[]を()に変えるところから試してみてください。

