Python: イテレータとジェネレータ
最終更新:2026-09-22
これまで
for x in list:を使ってデータを反復処理してきましたが、inの後には何が来るのか考えたことはありますか?なぜrange(1000000)はメモリを消費しないのでしょうか?イテレータとジェネレータがその答えです。これらにより、「無限」のデータストリームを処理できます——必要なときに生成され、メモリに保存されることはありません。
1. イテレータプロトコルとは
Python では、for ループで使用できるオブジェクトはすべてイテラブルです。これらはイテレータプロトコルを実装しています:
▶ サンプル:イテレータプロトコルの基本
▶ 試してみよう出力:
# 执行成功
for ループは本質的にこれを実行しています:
▶ サンプル:for ループを手動でシミュレート
▶ 試してみよう出力:
# 执行成功
2. カスタムイテレータ
__iter__ と __next__ を実装して、クラスを for ループで使えるようにします:
▶ サンプル:カウントダウンイテレータ
▶ 試してみよう出力:
# 函数定义成功
▶ サンプル:フィボナッチイテレータ(難易度 ⭐⭐)
▶ 試してみよう出力:
# 函数定义成功
3. ジェネレータ:yield
__iter__ と __next__ を持つクラスを書くのは面倒です。ジェネレータ関数は yield を使って同じことを実現します:
▶ サンプル:カウントダウンジェネレータ
▶ 試してみよう出力:
# 函数定义成功
yield は return とは異なります——return は関数を終了しますが、yield は関数を一時停止し、現在の状態を記憶し、次回の呼び出し時に中断したところから再開します。
▶ サンプル:フィボナッチジェネレータ
▶ 試してみよう出力:
# 函数定义成功
4. ジェネレータの遅延読み込み
ジェネレータの最大の利点——すべてのデータを一度に生成するのではなく、必要に応じて生成することです:
▶ サンプル:ジェネレータの遅延読み込み
▶ 試してみよう出力:
Generating 1
Generating 2
Generating 3
Generator created, but not yet executed
実行の流れを見てください。next() を呼び出すたびに、次の yield まで実行され、一時停止します。これは大規模データで非常に便利です:
▶ サンプル:大きなファイルを行ごとに読み取り
# 大きなファイルを処理する「遅延」方法
def read_large_file(filename):
"""大きなファイルを行ごとに読み取り、すべてをメモリに読み込まない"""
with open(filename, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# ジェネレータで処理 — 一度に 1 行のみメモリに保持
for line in read_large_file("huge_log.txt"):
if "ERROR" in line:
print(line) # 速度はディスク読み取り速度のみに制限される
5. ジェネレータ式
リスト内包表記と似ていますが、括弧を使用します——ジェネレータ式は遅延評価されます:
▶ サンプル:ジェネレータ式
▶ 試してみよう出力:
# 执行成功
| 比較 | リスト内包表記 [] |
ジェネレータ式 () |
|---|---|---|
| メモリ | すべてのデータがメモリに | 必要に応じて、ほぼゼロ |
| 速度 | 高速な生成とアクセス | 遅延、毎回計算 |
| 再利用 | 複数回反復可能 | 1 回のみ反復可能 |
| 使用ケース | 小規模データ、繰り返し使用 | 大規模データ、1 回だけの処理 |
6. よくあるユースケース
- ビッグデータ処理:大きなログ/CSV ファイルを行ごとに読み取り、OOM を回避
- 無限シーケンス:ジェネレータは「無限」のデータストリームを表現可能——フィボナッチ、センサーデータ
- パイプライン処理:複数のジェネレータを連鎖させてデータ処理パイプラインを構築
- 逆方向反復:
reversed()は一部の型でイテレータを返す - 遅延評価:関数がジェネレータを返し、呼び出し側が取得する値を決める
❓ よくある質問
return を使って結果を 1 回返し、終了します。ジェネレータは yield を使って 1 つずつ値を生成し、各 yield の後に一時停止して状態を保存します。ジェネレータ関数を呼び出すたびに、新しいジェネレータオブジェクトが返されます。list(gen) でジェネレータをリストに変換します。ただし、データが大きすぎる場合、リストに変換するとメモリが枯渇します。妥協策:itertools.tee() でジェネレータをコピーするか、ジェネレータ関数を再度呼び出して新しいジェネレータを作成します。yield と return は共存できますか?return は効果的に反復を停止し、オプションの値とともに StopIteration を発生させます。ただし、ほとんど使われません。yield と return の両方を使う関数が必要な場合は、設計を見直してください。📖 まとめ
- イテラブルオブジェクトは
forループをサポート。背後では__iter__と__next__プロトコルを実装 - 手動反復:
iter()でイテレータを取得、next()で次の値を取得、StopIterationで終了 - ジェネレータ関数は
yieldを使って 1 つずつ値を生成。状態を自動保存 - ジェネレータは遅延評価——必要な分だけ生成し、メモリを無駄にしない
- ジェネレータ式
(x for x in seq)はリスト内包表記と似ているが、遅延評価される yield fromは別のジェネレータに委譲(高度。必要なときに学ぶ)
📝 練習問題
-
基本(難易度 ⭐):ジェネレータ
even_numbers(max_n)を書いてください。0 からmax_nまでのすべての偶数を生成します。 -
中級(難易度 ⭐⭐):ジェネレータ
repeat_list(items, times)を書いてください。リストの各要素をtimes回繰り返します。例:list(repeat_list([1, 2, 3], 2))は[1, 1, 2, 2, 3, 3]を返します。 -
上級(難易度 ⭐⭐⭐):ジェネレータ
deep_flatten(nested)を書いてください。任意の深さのネストしたリストを平坦化します。例:list(deep_flatten([1, [2, [3, 4]], 5]))は[1, 2, 3, 4, 5]を返します。ヒント: 要素を反復処理し、要素がリストの場合はyield fromで再帰的に yield します。