メインコンテンツへスキップ

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

生成器によるストリーミングデータ処理図

この節の位置づけ

この節では、for ループの裏側にある仕組みを説明し、よりメモリ効率のよいデータ処理方法を紹介します。イテレータとジェネレータは、大きなファイル、ストリーミングデータ、学習データの読み込みを扱うときにとても役立ちます。まず考え方を理解してから、最もよく使う yield の書き方を身につけましょう。

学習目標

  • イテレータ規約(__iter____next__)を理解する
  • ジェネレータ関数(yield)の使い方を身につける
  • ジェネレータ式を理解する
  • なぜジェネレータが大規模データ処理で重要なのかを知る

イテレーションとは?

あなたはすでに何度も for ループを使っています。

for item in [1, 2, 3]:
print(item)

for char in "Hello":
print(char)

for key in {"a": 1, "b": 2}:
print(key)

for...in でこれらを順にたどれるのは、すべて**イテラブル(Iterable)**だからです。では、for ループの裏側では実際に何が起きているのでしょうか?


イテレータ規約

手動で反復する

for ループの本質は、次のようなものです。

numbers = [10, 20, 30]

# for ループの書き方
for n in numbers:
print(n)

# 同じ動きを手動で書くと
iterator = iter(numbers) # 1. イテレータを取得
print(next(iterator)) # 2. 次の要素を取得 → 10
print(next(iterator)) # 3. 次の要素を取得 → 20
print(next(iterator)) # 4. 次の要素を取得 → 30
# print(next(iterator)) # 5. もう要素がない → StopIteration を送出

イテレータ規約

  • iter(オブジェクト) → イテレータを取得する
  • next(イテレータ) → 次の要素を取得する
  • 要素がなくなったら StopIteration 例外を送出する

独自のイテレータを作る

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 num in Countdown(5):
print(num, end=" ")
# 出力: 5 4 3 2 1

ただし、イテレータを手で実装するのは少し面倒です。次に紹介するジェネレータは、もっと簡潔な方法です。


ジェネレータ関数(Generator)

ジェネレータは特別なイテレータで、return の代わりに yield キーワードを使います。

基本的な使い方

def countdown(n):
"""カウントダウンのジェネレータ"""
while n > 0:
yield n # 一時停止して n を返し、次回ここから続く
n -= 1

# 使い方はイテレータと同じ
for num in countdown(5):
print(num, end=" ")
# 出力: 5 4 3 2 1

yieldreturn の違い

# return: 関数が最後まで実行され、結果をまとめて返す
def get_squares_return(n):
result = []
for i in range(n):
result.append(i ** 2)
return result

# yield: 1つずつ返し、次の呼び出しまで一時停止する
def get_squares_yield(n):
for i in range(n):
yield i ** 2

# どちらも結果は同じ
print(list(get_squares_return(5))) # [0, 1, 4, 9, 16]
print(list(get_squares_yield(5))) # [0, 1, 4, 9, 16]

重要な違い:

特徴returnyield
戻り方まとめて全部返す1つずつ返す
メモリ使用量すべてメモリに載る必要に応じて生成し、ほとんどメモリを使わない
実行方法最後まで実行する一時停止 / 再開する

ジェネレータの実行の流れ

def simple_gen():
print("1つ目")
yield 1
print("2つ目")
yield 2
print("3つ目")
yield 3
print("終了")

gen = simple_gen() # ジェネレータを作るだけで、まだコードは実行しない

print(next(gen)) # 最初の yield まで実行。 "1つ目" を表示し、1 を返す
print(next(gen)) # 前回の停止位置から再開。 "2つ目" を表示し、2 を返す
print(next(gen)) # "3つ目" を表示し、3 を返す
# next(gen) # "終了" を表示してから StopIteration を送出

出力:

1つ目
1
2つ目
2
3つ目
3

なぜジェネレータが必要なのか?—— 大規模データの処理

これは、ジェネレータの最も重要な使いどころです。

問題:一度に大量のデータを読み込む

# たとえば 10GB のファイルを処理するとします
# 悪い方法: すべての行を一度に読み込む
lines = open("huge_file.txt").readlines() # 💥 メモリ不足になる!

# 正しい方法: ジェネレータで1行ずつ処理する
def read_large_file(filepath):
with open(filepath, "r") as f:
for line in f: # ファイルオブジェクト自体がイテレータ。1行ずつ読める
yield line.strip()

for line in read_large_file("huge_file.txt"):
process(line) # メモリ上には常に1行だけある

メモリ使用量の比較

import sys

# リスト: すべての要素がメモリ上にある
big_list = [i ** 2 for i in range(1_000_000)]
print(f"リストのメモリ使用量: {sys.getsizeof(big_list):,} バイト") # 約 8MB

# ジェネレータ: 現在の状態だけを覚えている
big_gen = (i ** 2 for i in range(1_000_000))
print(f"ジェネレータのメモリ使用量: {sys.getsizeof(big_gen):,} バイト") # 約 200 バイト!

8MB と 200 バイトでは、4万倍もの差があります。データ量がさらに大きくなると、たとえば数百万件の学習データを処理するとき、この差は「動くかどうか」と「メモリ不足で落ちるかどうか」の違いになります。


ジェネレータ式

リスト内包表記の []() に変えると、ジェネレータ式になります。

# リスト内包表記 → すぐにすべての要素を作る
squares_list = [x ** 2 for x in range(10)]

# ジェネレータ式 → 必要に応じて生成する
squares_gen = (x ** 2 for x in range(10))

print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>

# ジェネレータ式は関数の引数でよく使う
total = sum(x ** 2 for x in range(1000)) # 追加の括弧は不要
print(total)

max_score = max(s["score"] for s in students)

実用的なジェネレータのパターン

無限列

def infinite_counter(start=0, step=1):
"""無限カウンター"""
n = start
while True:
yield n
n += step

# 最初の10個の偶数を生成する
counter = infinite_counter(0, 2)
for _ in range(10):
print(next(counter), end=" ")
# 0 2 4 6 8 10 12 14 16 18

データパイプライン

ジェネレータは連結して使うことで、データ処理のパイプラインを作れます。

def read_lines(filename):
"""ファイルの各行を読む"""
with open(filename) as f:
for line in f:
yield line.strip()

def filter_comments(lines):
"""コメント行を除外する"""
for line in lines:
if not line.startswith("#") and line:
yield line

def parse_numbers(lines):
"""各行を数値に変換する"""
for line in lines:
try:
yield float(line)
except ValueError:
continue # 変換できない行はスキップする

# パイプラインを組み合わせる: 読み込み → フィルタ → 変換
# メモリ上には常に1行分のデータしかない!
sample = ["# note", "1", "2.5", "bad", "4"]
numbers = parse_numbers(filter_comments(sample))
total = sum(numbers)
print(total)

バッチ処理

def batch(iterable, size):
"""データを一定サイズのバッチに分ける"""
batch_data = []
for item in iterable:
batch_data.append(item)
if len(batch_data) == size:
yield batch_data
batch_data = []
if batch_data: # 最後にバッチにならなかった分
yield batch_data

# 学習データのバッチ処理をまねる
data = list(range(1, 11)) # [1, 2, 3, ..., 10]

for b in batch(data, 3):
print(f"処理中のバッチ: {b}")
# 処理中のバッチ: [1, 2, 3]
# 処理中のバッチ: [4, 5, 6]
# 処理中のバッチ: [7, 8, 9]
# 処理中のバッチ: [10]

itertools: イテレータの便利ツール集

Python 標準ライブラリの itertools には、便利なイテレータツールがたくさんあります。

import itertools

# chain: 複数のイテレータをつなぐ
for item in itertools.chain([1, 2], [3, 4], [5, 6]):
print(item, end=" ") # 1 2 3 4 5 6

# islice: イテレータのスライス(ジェネレータで便利)
gen = (x ** 2 for x in range(100))
first_five = list(itertools.islice(gen, 5))
print(first_five) # [0, 1, 4, 9, 16]

# zip_longest: 長さが違うときに埋める
names = ["山田", "佐藤", "鈴木"]
scores = [85, 92]
for name, score in itertools.zip_longest(names, scores, fillvalue="欠席"):
print(f"{name}: {score}")
# 山田: 85, 佐藤: 92, 鈴木: 欠席

# product: デカルト積
for combo in itertools.product(["赤", "青"], ["大", "小"]):
print(combo)
# ('赤', '大'), ('赤', '小'), ('青', '大'), ('青', '小')

# count: 無限カウント
for i in itertools.islice(itertools.count(10, 5), 5):
print(i, end=" ") # 10 15 20 25 30

総合例: AI データローダー

import random

def data_loader(dataset, batch_size=32, shuffle=True):
"""
AI 学習用のデータローダーをまねる。
ジェネレータで実装しているので、メモリ効率がよい。
"""
indices = list(range(len(dataset)))

if shuffle:
random.shuffle(indices)

for start in range(0, len(indices), batch_size):
batch_indices = indices[start:start + batch_size]
batch_data = [dataset[i] for i in batch_indices]
yield batch_data

# 例のデータセット
dataset = [f"sample_{i}" for i in range(100)]

# 学習ループ
for epoch in range(3):
print(f"\n=== Epoch {epoch + 1} ===")
for batch_idx, batch in enumerate(data_loader(dataset, batch_size=32)):
print(f" Batch {batch_idx + 1}: {len(batch)} 個のサンプル "
f"(最初: {batch[0]}, 最後: {batch[-1]})")

手を動かしてみよう

練習 1: フィボナッチのジェネレータ

def fibonacci(n=None):
"""フィボナッチ数を生成する。n が None なら無限に生成する。"""
count = 0
a, b = 0, 1
while n is None or count < n:
yield a
a, b = b, a + b
count += 1

for num in fibonacci(10):
print(num, end=" ")
# 0 1 1 2 3 5 8 13 21 34

練習 2: ファイル検索器

from pathlib import Path

def search_files(directory, pattern):
"""pattern に一致するファイルパスを再帰的に生成する。"""
yield from Path(directory).rglob(pattern)

for filepath in search_files(".", "*.py"):
print(filepath)

練習 3: スライディングウィンドウ

def sliding_window(data, window_size):
"""固定サイズのスライディングウィンドウを生成する。"""
for index in range(len(data) - window_size + 1):
yield data[index:index + window_size]

for window in sliding_window([1, 2, 3, 4, 5], 3):
print(window)

まとめ

概念説明重要ポイント
イテレータ__iter____next__ を実装したオブジェクトfor ループの基礎
ジェネレータ関数yield を含む関数イテレータを簡単に作れる
ジェネレータ式(x for x in iterable)リスト内包表記の遅延版
yield関数を一時停止して値を返す次回呼び出し時に停止位置から再開する
itertools標準ライブラリのイテレータツール集chain, islice, product など
核心となる理解

ジェネレータの本質は**遅延評価(Lazy Evaluation)**です。結果を一度にすべて計算するのではなく、必要になったときに1つずつ計算します。これは、ビュッフェとデリバリーの違いのようなものです。リストは料理を全部まとめてテーブルに並べるイメージ(テーブルがいっぱいになる)、ジェネレータは料理を1品ずつ出すイメージ(テーブルの上にはいつも1皿だけ)です。大規模データセットやデータストリームを扱うとき、ジェネレータは欠かせない道具です。