イテレータとジェネレータ

これまで for x in list: を使ってデータを反復処理してきましたが、in の後には何が来るのか考えたことはありますか?なぜ range(1000000) はメモリを消費しないのでしょうか?イテレータジェネレータがその答えです。これらにより、「無限」のデータストリームを処理できます——必要なときに生成され、メモリに保存されることはありません。


1. イテレータプロトコルとは

Python では、for ループで使用できるオブジェクトはすべてイテラブルです。これらはイテレータプロトコルを実装しています:

例:イテレータプロトコルの基本

PYTHON
# すべてのコンテナはイテラブル
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 ループを手動でシミュレート

PYTHON
numbers = [1, 2, 3]
it = iter(numbers)
while True:
    try:
        value = next(it)
        print(value)
    except StopIteration:
        break
▶ 試してみよう

2. カスタムイテレータ

__iter____next__ を実装して、クラスを for ループで使えるようにします:

例:カウントダウンイテレータ

PYTHON
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
▶ 試してみよう

例:フィボナッチイテレータ(難易度 ⭐⭐)

PYTHON
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 を使って同じことを実現します:

例:カウントダウンジェネレータ

PYTHON
def countdown(start):
    """カウントダウンジェネレータ"""
    while start > 0:
        yield start
        start -= 1

# 使用例
for i in countdown(5):
    print(i, end=" ")                   # 5 4 3 2 1
▶ 試してみよう

yieldreturn とは異なります——return は関数を終了しますが、yield は関数を一時停止し、現在の状態を記憶し、次回の呼び出し時に中断したところから再開します。

例:フィボナッチジェネレータ

PYTHON
# 同じフィボナッチ — 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
▶ 試してみよう
💡 ジェネレータ vs イテレータクラス: ジェネレータ関数(yield)は通常、手書きのイテレータクラスよりはるかに簡潔です。ほとんどのシナリオでは、ジェネレータが第一選択です。複雑な状態を維持する必要がある場合や追加のメソッドが必要な場合にのみ、イテレータクラスの作成を検討してください。


4. ジェネレータの遅延読み込み

ジェネレータの最大の利点——すべてのデータを一度に生成するのではなく、必要に応じて生成することです:

例:ジェネレータの遅延読み込み

PYTHON
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 まで実行され、一時停止します。これは大規模データで非常に便利です:

例:大きなファイルを行ごとに読み取り

PYTHON
# 大きなファイルを処理する「遅延」方法
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. ジェネレータ式

リスト内包表記と似ていますが、括弧を使用します——ジェネレータ式は遅延評価されます:

例:ジェネレータ式

PYTHON
# リスト内包表記 — すべてのデータを一度に生成
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 回だけの処理
💡 ジェネレータを使うタイミング: ① 大規模データセット(数千項目以上)。② 1 回だけ反復すれば十分な場合。③ ストリーミング処理が必要な場合(行ごとのファイル、リアルタイムデータストリーム)。④「無限シーケンス」を表現したい場合。


よくあるユースケース


❓ よくある質問

Q ジェネレータと通常の関数の違いは何ですか?
A 通常の関数は return を使って結果を 1 回返し、終了します。ジェネレータは yield を使って 1 つずつ値を生成し、各 yield の後に一時停止して状態を保存します。ジェネレータ関数を呼び出すたびに、新しいジェネレータオブジェクトが返されます。
Q ジェネレータは 1 回しか反復できません。複数回反復する必要がある場合はどうすればよいですか?
A list(gen) でジェネレータをリストに変換します。ただし、データが大きすぎる場合、リストに変換するとメモリが枯渇します。妥協策:itertools.tee() でジェネレータをコピーするか、ジェネレータ関数を再度呼び出して新しいジェネレータを作成します。
Q yieldreturn は共存できますか?
A はい——ジェネレータ内の return は効果的に反復を停止し、オプションの値とともに StopIteration を発生させます。ただし、ほとんど使われません。yieldreturn の両方を使う関数が必要な場合は、設計を見直してください。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):ジェネレータ even_numbers(max_n) を書いてください。0 から max_n までのすべての偶数を生成します。

  2. 中級(難易度 ⭐⭐):ジェネレータ repeat_list(items, times) を書いてください。リストの各要素を times 回繰り返します。例:list(repeat_list([1, 2, 3], 2))[1, 1, 2, 2, 3, 3] を返します。

  3. 上級(難易度 ⭐⭐⭐):ジェネレータ deep_flatten(nested) を書いてください。任意の深さのネストしたリストを平坦化します。例:list(deep_flatten([1, [2, [3, 4]], 5]))[1, 2, 3, 4, 5] を返します。ヒント: 要素を反復処理し、要素がリストの場合は yield from で再帰的に yield します。

100%