📚 対象読者: プログラミングの基礎はあるが、ジェネレータは初めての方
⏱️ 学習時間: 約 30〜40 分(コードを試しながら)
🔗 次回: #2 イテレータの仕組み
🤔 こんな経験ありませんか?
大きなファイルを処理しようとして、こんなコードを書いたことはないでしょうか?
# ログファイル全体を読み込んで処理
with open(filename) as f:
all_lines = f.readlines() # 全行を一気に読み込み → 💥 メモリ不足でクラッシュ!
こういう問題、エンジニアなら誰でも一度は経験しますよね。
「全部のデータを一度にメモリに載せないといけないのかな...?」
「必要な分だけ、ちょっとずつ処理できたらいいのに...」
そう思ったあなたに朗報です! Python にはそのための機能があります。
それが ジェネレータ です。
📜 豆知識:ジェネレータはどう生まれた?
2001 年の革命
2001 年、Python 2.2 で PEP 255 という提案が採用されました。
これが ジェネレータ の誕生です。
当時の Python 開発者たちも、私たちと同じ悩みを抱えていました:
- 📁 巨大なファイルを処理するとメモリが足りない
- 🔄 無限のデータ列を扱いたい(でもリストは有限)
- ⚡ 処理の開始を早くしたい(全部生成するまで待ちたくない)
そこで考えられたのが「遅延評価」という発想です。
「全部を一度に作るんじゃなくて、必要な時に必要な分だけ作ればいいじゃん!」
この考え方を実現したのが yield キーワードでした。
🎯 ジェネレータって何?
簡単に言うと、値を一つずつ返してくれる関数です。
普通の関数は return で一度に全部を返しますが、ジェネレータは yield で一つずつ返します。
そして、前回の続きから再開できるんです。これが超便利!
百聞は一見にしかず:比較してみよう
❌ 従来の方法:全部作ってから返す
def count_to_million():
"""100万個の数を全部リストで返す"""
result = []
for i in range(1_000_000):
result.append(i)
return result # ドカンと全部返す
numbers = count_to_million() # 待ち時間長い&メモリ食う
print(numbers[0]) # 0
print(numbers[1]) # 1
# 最初の2つしか使わないのに、100万個全部作ってる...😢
✅ ジェネレータ:一つずつ作って返す
def count_to_million():
"""必要な時に一つずつ数を返す"""
for i in range(1_000_000):
yield i # 一つ返して、また呼ばれるまで待機
numbers = count_to_million() # 一瞬で終わる!
print(next(numbers)) # 0(今作った)
print(next(numbers)) # 1(今作った)
# 必要な分だけ、その場で作る!😊
違いがわかりますか?
- 普通の関数:お弁当を 100 個全部作ってから渡す 🍱🍱🍱...(重い)
- ジェネレータ:注文が来たら 1 個ずつ作って渡す 🍱→🍱→🍱(軽い)
もう一つ例を見てみましょう
ファイル処理の場合:
# ❌ 従来:ファイル全体を読む
def read_file_old(filename):
with open(filename) as f:
return f.readlines() # 全行を一気にメモリへ
lines = read_file_old('huge.log') # 1GB のファイル → メモリ 1GB 消費
for line in lines:
if 'ERROR' in line:
print(line)
break # 最初の ERROR が見つかっても、すでに全部読んじゃってる
# ✅ ジェネレータ:1行ずつ読む
def read_file_new(filename):
with open(filename) as f:
for line in f:
yield line.strip() # 1行返して待機
lines = read_file_new('huge.log') # メモリほぼ使わない
for line in lines:
if 'ERROR' in line:
print(line)
break # ERROR が見つかったらすぐ終了!残りは読まない
これがジェネレータの魅力です! 🎉
📊 普通の関数 vs ジェネレータ:違いを理解する
コード例で比較
# 普通の関数
def normal():
return 1 # ← ここで終了、変数は消える
# ジェネレータ
def generator():
yield 1 # ← ここで停止、変数は保持される
yield 2 # ← 続きから再開できる
詳細比較表
| 特徴 | 普通の関数 | ジェネレータ |
|---|---|---|
| キーワード | return |
yield |
| 呼び出し時 | 即座に実行開始 | 何もしない(オブジェクト生成のみ) |
| 値の取得 | 関数呼び出し | next(gen) |
| 値の返し方 |
return で 1 回だけ |
yield で複数回可能 |
| 関数の終了 |
return で即座に終了 |
yield で一時停止、next() で再開 |
| ローカル変数 | 終了後に消える | 一時停止中も保持される |
| 実行位置 | 保存されない | 停止位置が保存される |
| 再開 | 不可能(毎回最初から) | 可能(続きから実行) |
| メモリ | 全データを一度に生成 | 必要な時に 1 つずつ生成 |
| 用途 | 計算結果を返す | データストリーム、遅延評価 |
🎯 基礎編:まずは使ってみよう
1. yield の基本動作を理解する
まずは超シンプルな例から:
def simple_gen():
print("1個目を作ります")
yield "りんご"
print("2個目を作ります")
yield "みかん"
print("3個目を作ります")
yield "バナナ"
print("終わりです")
# 使ってみる
fruits = simple_gen()
print(next(fruits)) # 「1個目を作ります」→ りんご
print(next(fruits)) # 「2個目を作ります」→ みかん
print(next(fruits)) # 「3個目を作ります」→ バナナ
# print(next(fruits)) # 「終わりです」→ StopIteration エラー
ポイント:
-
yieldで値を返すと、そこで一時停止 ⏸️ -
next()を呼ぶと、続きから再開 ▶️ - 関数の中の変数はそのまま保持される
これが return との大きな違いです!
2. for ループで使う(実務で一番多い)
実は、for ループは自動的に next() を呼んでくれます:
def countdown(n):
"""カウントダウンするジェネレータ"""
while n > 0:
yield n
n -= 1
# for ループで簡単に使える
for num in countdown(5):
print(f"{num}...")
# 出力:
# 5...
# 4...
# 3...
# 2...
# 1...
print("発射!🚀")
便利なポイント:
# 必要な分だけ取る
numbers = []
for num in countdown(1000):
numbers.append(num)
if len(numbers) == 3:
break
print(numbers) # [1000, 999, 998]
3. フィボナッチ数列(無限に生成できる!)
ジェネレータの真骨頂:無限の数列も扱える
def fibonacci():
"""フィボナッチ数列を無限に生成"""
a, b = 0, 1
while True: # 無限ループ!でも大丈夫
yield a
a, b = b, a + b
# 必要な分だけ取得
fib = fibonacci()
for i in range(10):
print(next(fib), end=" ")
# 出力: 0 1 1 2 3 5 8 13 21 34
なぜ無限ループでも大丈夫?
yield があるから、その都度停止します。
呼ばれない限り、次の計算はしません。これが遅延評価です!
実用例:大きなフィボナッチ数を探す
def find_first_fib_over(threshold):
"""指定値を超える最初のフィボナッチ数を探す"""
for fib_num in fibonacci():
if fib_num > threshold:
return fib_num
print(find_first_fib_over(1000)) # 1597
# 無限に生成できるけど、見つかったらすぐ終了!
🚀 応用編:実務で使えるテクニック
基礎がわかったら、実際のプロジェクトで使えるパターンを見ていきましょう。
1. 大きなファイルを効率的に処理する
実務で一番多いパターンです。ログ分析、CSV 処理など:
def analyze_large_log(filename):
"""ギガバイト級のログファイルも軽々処理"""
error_count = 0
warning_count = 0
with open(filename, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
# エラー行だけを返す
if 'ERROR' in line:
error_count += 1
yield {
'type': 'ERROR',
'line': line_num,
'content': line.strip()
}
elif 'WARNING' in line:
warning_count += 1
yield {
'type': 'WARNING',
'line': line_num,
'content': line.strip()
}
# 使用例:10GB のログファイルでも大丈夫
issues = analyze_large_log('production.log')
# 最初の10件だけチェック
for i, issue in enumerate(issues):
print(f"{issue['type']} at line {issue['line']}")
if i >= 9:
break
# メモリはほとんど使わない!
実用的な応用:
# 特定のユーザーのログだけ抽出
def filter_user_logs(log_generator, user_id):
"""特定ユーザーのログだけ"""
for log in log_generator:
if f"user_id={user_id}" in log['content']:
yield log
# チェーンできる
all_logs = analyze_large_log('app.log')
user_logs = filter_user_logs(all_logs, 'user_12345')
for log in user_logs:
print(log)
2. データのパイプライン処理
複数の処理を繋げて、ストリーム処理ができます:
def read_csv_rows(filename):
"""CSVを1行ずつ読む"""
import csv
with open(filename, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
yield row
def clean_data(rows):
"""データをクリーニング"""
for row in rows:
# 空白を削除、小文字化
cleaned = {
key: value.strip().lower() if value else None
for key, value in row.items()
}
yield cleaned
def filter_adults(rows):
"""成人だけフィルタリング"""
for row in rows:
age = int(row.get('age', 0))
if age >= 20:
yield row
def add_category(rows):
"""年齢カテゴリを追加"""
for row in rows:
age = int(row['age'])
if age < 30:
row['category'] = '若年層'
elif age < 60:
row['category'] = '中年層'
else:
row['category'] = 'シニア層'
yield row
# パイプライン構築(Unix のパイプみたい!)
pipeline = read_csv_rows('users.csv')
pipeline = clean_data(pipeline)
pipeline = filter_adults(pipeline)
pipeline = add_category(pipeline)
# 処理実行(必要な分だけ)
for user in pipeline:
print(f"{user['name']}: {user['category']}")
# 100万行あっても、メモリは数KB程度
ポイント:
- 各ステップが独立しているので、テストしやすい
- 途中で
breakすれば、それ以降は処理されない - メモリ効率が最高
3. バッチ処理とレート制限
詳細を見る(機械学習やAPI制限で使う高度なテクニック)
バッチ処理
データを固まりに分けて処理:
def batch_data(items, batch_size):
"""データをバッチに分割"""
batch = []
for item in items:
batch.append(item)
if len(batch) == batch_size:
yield batch
batch = []
# 最後の余り
if batch:
yield batch
# 実用例:機械学習のミニバッチ
def train_model(data_file, batch_size=32):
"""ニューラルネットワークを学習"""
def load_images(filename):
"""画像を1枚ずつ読み込む"""
with open(filename) as f:
for line in f:
image_path = line.strip()
image = load_image(image_path) # 重い処理
yield image
# バッチごとに学習
images = load_images(data_file)
for batch_num, batch in enumerate(batch_data(images, batch_size)):
loss = model.train_on_batch(batch)
print(f"Batch {batch_num}: loss = {loss}")
# 10バッチだけ試したい場合
if batch_num >= 9:
break
# メモリには32枚分の画像しか載らない!
API リクエストの制限
def rate_limited_requests(urls, requests_per_second=10):
"""API レートリミット対策"""
import time
for i, url in enumerate(urls):
yield fetch_url(url)
# 1秒に10リクエストまで
if (i + 1) % requests_per_second == 0:
time.sleep(1)
# 使用例
urls = get_all_urls() # 10万個のURL
for data in rate_limited_requests(urls):
process_data(data)
# 自動的にレートリミットが守られる
🎓 さらに深く学ぶ
ジェネレータの使い方をマスターしましたね!
より深く理解したい方は、理論編へ:
📘 #1.5-理論編:内部動作を深く理解する
理論編では、以下のトピックを詳しく解説しています:
- 🔍 メリット・デメリットの詳細
- 💾 メモリ管理の仕組み(変数の削除タイミング)
- ⚠️ メモリリークの対策
- ⚡ パフォーマンス比較
- 📋 ベストプラクティス
理論編を読むことで、より効率的で安全なコードが書けるようになります!
🎓 まとめ
お疲れ様でした!ジェネレータの基礎、マスターできましたね 🎉
今日覚えておきたいこと
ジェネレータの本質:
-
returnじゃなくてyieldを使う - 値を一つずつ返して、その都度停止
- 必要な時だけ計算する(遅延評価)
- メモリ効率が超良い
使うべき場面:
- ✅ 大きなファイルを処理する時
- ✅ メモリを節約したい時
- ✅ 無限の数列が必要な時
- ✅ データを途中で打ち切りたい時
使わなくて OK な場面:
- ❌ データが少量(数百件以下)
- ❌ 何度も同じデータにアクセスしたい
- ❌ ランダムアクセスが必要(
data[100]みたいな)
クイック比較表
| リスト | ジェネレータ |
|---|---|
| 🔴 メモリ食う | 🟢 メモリ効率的 |
| 🟢 何度でも使える | 🔴 一度しか使えない |
🟢 len() できる |
🔴 len() できない |
| 🔴 全部作るまで待つ | 🟢 すぐ使い始められる |
💪 ちょっと練習してみよう
問題:1 から指定された数までの偶数を返すジェネレータを作ってみてください
def even_numbers(n):
# ここに実装
pass
# テスト
for num in even_numbers(10):
print(num) # 2, 4, 6, 8, 10
💡 答えを見る
def even_numbers(n):
for i in range(1, n + 1):
if i % 2 == 0:
yield i
# または、もっとシンプルに
def even_numbers(n):
for i in range(2, n + 1, 2):
yield i
簡単でしたか?これだけ理解できていれば、実務でジェネレータを使えます!
🚀 次回予告
次回は「イテレータの仕組み」を解説します!
ジェネレータは実は「イテレータ」という仕組みの上に作られています。
次回は、その舞台裏を覗いてみましょう。
🔍 iter と next の秘密
🛠️ 自分でイテレータを作る方法
🎯 for ループが実際に何をしているか
📚 もっと学びたい方へ
- PEP 255 -- Simple Generators - ジェネレータが生まれた背景
- Python 公式ドキュメント - 日本語版
- Real Python - Generators - 詳しい解説(英語)
🎉 ここまで読んでくれてありがとう!
質問や「こんな例が知りたい」というリクエストがあれば、ぜひ教えてください 😊