イテレータとジェネレータ
これまで
for x in list:を使ってデータを反復処理してきましたが、inの後には何が来るのか考えたことはありますか?なぜrange(1000000)はメモリを消費しないのでしょうか?イテレータとジェネレータがその答えです。これらにより、「無限」のデータストリームを処理できます——必要なときに生成され、メモリに保存されることはありません。
1. イテレータプロトコルとは
Python では、for ループで使用できるオブジェクトはすべてイテラブルです。これらはイテレータプロトコルを実装しています:
例:イテレータプロトコルの基本
# すべてのコンテナはイテラブル
print(hasattr([1, 2, 3], "__iter__")) # True
print(hasattr("abc", "__iter__")) # True
print(hasattr(100, "__iter__")) # False — 数値はイテラブルではない
# 手動での反復処理
numbers = [1, 2, 3]
iterator = iter(numbers) # イテレータを取得
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
# print(next(iterator)) # StopIteration — 反復完了
for ループは本質的にこれを実行しています:
例:for ループを手動でシミュレート
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
value = next(it)
print(value)
except StopIteration:
break
2. カスタムイテレータ
__iter__ と __next__ を実装して、クラスを for ループで使えるようにします:
例:カウントダウンイテレータ
class Countdown:
"""カウントダウンイテレータ"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self # イテレータは自身を返す
def __next__(self):
if self.current <= 0:
raise StopIteration # 反復停止
value = self.current
self.current -= 1
return value
# 使用例
for i in Countdown(5):
print(i, end=" ") # 5 4 3 2 1
例:フィボナッチイテレータ(難易度 ⭐⭐)
class Fibonacci:
"""フィボナッチイテレータ — 最初の N 個の数を生成"""
def __init__(self, count):
self.count = count
self.index = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.index >= self.count:
raise StopIteration
self.index += 1
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
for num in Fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
3. ジェネレータ:yield
__iter__ と __next__ を持つクラスを書くのは面倒です。ジェネレータ関数は yield を使って同じことを実現します:
例:カウントダウンジェネレータ
def countdown(start):
"""カウントダウンジェネレータ"""
while start > 0:
yield start
start -= 1
# 使用例
for i in countdown(5):
print(i, end=" ") # 5 4 3 2 1
yield は return とは異なります——return は関数を終了しますが、yield は関数を一時停止し、現在の状態を記憶し、次回の呼び出し時に中断したところから再開します。
例:フィボナッチジェネレータ
# 同じフィボナッチ — yield を使うと、イテレータクラスよりはるかに簡潔
def fibonacci(count):
a, b = 0, 1
for _ in range(count):
yield a
a, b = b, a + b
for num in fibonacci(10):
print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
4. ジェネレータの遅延読み込み
ジェネレータの最大の利点——すべてのデータを一度に生成するのではなく、必要に応じて生成することです:
例:ジェネレータの遅延読み込み
def generate_numbers():
"""1 つずつ数値を生成するシミュレーション"""
print("Generating 1")
yield 1
print("Generating 2")
yield 2
print("Generating 3")
yield 3
gen = generate_numbers()
print("Generator created, but not yet executed")
print(next(gen)) # Generating 1 \n 1
print(next(gen)) # Generating 2 \n 2
print(next(gen)) # Generating 3 \n 3
実行の流れを見てください。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. ジェネレータ式
リスト内包表記と似ていますが、括弧を使用します——ジェネレータ式は遅延評価されます:
例:ジェネレータ式
# リスト内包表記 — すべてのデータを一度に生成
squares_list = [x ** 2 for x in range(1000000)]
print(f"List size: {len(squares_list)}") # 1000000(メモリを使用)
# ジェネレータ式 — 必要に応じて
squares_gen = (x ** 2 for x in range(1000000))
print(f"Generator: {squares_gen}") # generator object(メモリをほぼ使用しない)
# 同じ使い方 — for ループ
for i, val in enumerate(squares_gen):
if i >= 5:
break
print(val, end=" ") # 0 1 4 9 16
| 比較 | リスト内包表記 [] |
ジェネレータ式 () |
|---|---|---|
| メモリ | すべてのデータがメモリに | 必要に応じて、ほぼゼロ |
| 速度 | 高速な生成とアクセス | 遅延、毎回計算 |
| 再利用 | 複数回反復可能 | 1 回のみ反復可能 |
| 使用ケース | 小規模データ、繰り返し使用 | 大規模データ、1 回だけの処理 |
よくあるユースケース
- ビッグデータ処理:大きなログ/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 します。



