📚 対象読者: ジェネレータの基本を理解し、より深く学びたい方
⏱️ 学習時間: 約 30〜40 分(じっくり理解しながら)
🔗 前回: #1 yield の基礎
🔗 次回: #2 イテレータの仕組み
📖 この記事について
#1 yield の基礎 でジェネレータの使い方を学びました。
この記事では、内部で何が起きているのかを深く掘り下げます。
📚 目次
この記事で学ぶ内容:
Part 1: メリット・デメリット
- ✅ ジェネレータの 4 つのメリット
- ❌ 知っておくべき 5 つのデメリット
- 💡 リストへの変換の注意点
Part 2: メモリ管理を理解する
- 🗑️ ローカル変数が削除される 3 つのタイミング
- ⚠️ メモリリークの対策
- 💾 効率的なメモリ使用方法
Part 3: パフォーマンスとベストプラクティス
- ⚡ リストとの速度比較
- 📋 実務で使える 4 つのベストプラクティス
- 🎯 ジェネレータ式の活用
Part 1: メリット・デメリット
「yield って、何が良くて、何に注意すべき?」
まずは全体像を把握しましょう。
✅ メリット
| メリット | 説明 |
|---|---|
| 🟢 メモリ効率 | 全データを保持しないので省メモリ |
| 🟢 遅延評価 | 必要な時だけ計算するので高速 |
| 🟢 無限対応 | 無限のデータ列も扱える |
| 🟢 途中終了 | 必要な分だけ取得して終了できる |
❌ デメリット
| デメリット | 説明 |
|---|---|
| 🔴 一度しか使えない | 使い切ったら、もう一度関数を呼ぶ必要がある |
| 🔴 ランダムアクセス不可 |
gen[5] のような指定ができない |
| 🔴 サイズ不明 |
len(gen) で長さを取得できない |
| 🔴 メモリに常駐 | ジェネレータが終了するまで変数を保持し続ける |
| 🔴 わずかなオーバーヘッド | 状態の保存・復元に時間がかかる(ごくわずか) |
デメリットの実例
一度しか使えない
def gen():
yield 1
yield 2
g = gen()
print(list(g)) # [1, 2]
print(list(g)) # [] ← もう空!
# もう一度使うには再度呼び出す
g = gen()
print(list(g)) # [1, 2]
Part 2: メモリ管理を理解する
「変数はいつまでメモリに残るの?」
ジェネレータのメモリ管理を深く理解しましょう。
ローカル変数が削除される 3 つのタイミング
ジェネレータのローカル変数は、普通の関数より長くメモリに残ります。
1. ジェネレータが終了した時
def countdown():
n = 100 # メモリに確保
yield n
# ここで関数終了
gen = countdown()
next(gen) # n = 100 がメモリにある
next(gen) # StopIteration → n が削除される ✅
2. ジェネレータオブジェクトを削除した時
def counter():
n = 0
while True:
yield n
n += 1
gen = counter() # n = 0 をメモリに確保
next(gen) # 0
next(gen) # 1
# まだ n はメモリにある...
del gen # ジェネレータ削除 → n も削除される ✅
3. 参照がなくなった時(ガベージコレクション)
def process():
data = [1, 2, 3] # メモリに確保
for item in data:
yield item
# この関数が終わったら...
def use_generator():
gen = process() # data がメモリにある
for val in gen:
print(val)
# 関数終了 → gen の参照が消える → data が削除される ✅
use_generator()
⚠️ メモリリークに注意
ジェネレータ内でリストなどに蓄積し続けると、メモリリークが発生します。
❌ 悪い例:
def infinite_gen():
history = [] # ← これがどんどん大きくなる
n = 0
while True:
history.append(n) # メモリに蓄積
yield n
n += 1
gen = infinite_gen()
for i in range(1_000_000):
next(gen)
# history に 100万個の要素が残っている!
✅ 良い例:
def infinite_gen():
n = 0 # これだけなら問題ない
while True:
yield n
n += 1
履歴が必要な場合の対策
方法 1: 最新 N 件だけ保持
from collections import deque
def generator_with_history(max_history=100):
history = deque(maxlen=max_history) # 最新100件だけ
n = 0
while True:
history.append(n)
yield n, list(history)
n += 1
方法 2: 外部で管理
def simple_gen():
n = 0
while True:
yield n
n += 1
# 履歴は外で管理
gen = simple_gen()
history = []
for i in range(1000):
val = next(gen)
history.append(val)
if len(history) > 100:
history.pop(0) # 古いものを削除
Part 3: パフォーマンスとベストプラクティス
「実際どれくらい速いの?どう使うべき?」
実践的なテクニックを学びましょう。
パフォーマンス比較
全部使う場合
import time
# リスト方式
start = time.perf_counter()
data = [i for i in range(1_000_000)]
for item in data:
pass
end = time.perf_counter()
print(f"リスト: {(end - start) * 1000:.2f}ms")
# ジェネレータ方式
start = time.perf_counter()
data = (i for i in range(1_000_000))
for item in data:
pass
end = time.perf_counter()
print(f"ジェネレータ: {(end - start) * 1000:.2f}ms")
結果:ほぼ同じ速度
途中で止める場合
# リスト方式
start = time.perf_counter()
data = [i for i in range(1_000_000)]
for i, item in enumerate(data):
if i == 10:
break
end = time.perf_counter()
print(f"リスト: {(end - start) * 1000:.2f}ms")
# ジェネレータ方式
start = time.perf_counter()
data = (i for i in range(1_000_000))
for i, item in enumerate(data):
if i == 10:
break
end = time.perf_counter()
print(f"ジェネレータ: {(end - start) * 1000:.2f}ms")
結果:ジェネレータが数千倍速い
ベストプラクティス:実務で使えるテクニック
1. 大きなデータは必ずジェネレータで
# ❌ 避ける
def read_large_file(filename):
with open(filename) as f:
return f.readlines() # 全部読む
# ✅ 推奨
def read_large_file(filename):
with open(filename) as f:
for line in f:
yield line.strip()
2. ジェネレータ式を活用
# リスト内包表記
squares = [x**2 for x in range(1000000)] # メモリ消費
# ジェネレータ式
squares = (x**2 for x in range(1000000)) # メモリ効率的
💡 豆知識:ジェネレータ式と yield の関係
ジェネレータ式には yield が見えませんが、実は 内部で自動的に yield を使っています。
# ジェネレータ式(シンタックスシュガー)
squares = (x**2 for x in range(10))
# 上記は、内部的にこれと同じ
def _generator():
for x in range(10):
yield x**2
squares = _generator()
Python が自動的にジェネレータ関数を作ってくれているので、yield を書く必要がないんです!
どちらを使う?
- 簡単な処理 → ジェネレータ式
(x**2 for x in range(10)) - 複雑な処理 → ジェネレータ関数
def gen(): yield ...
3. 再利用が必要ならリストに変換
gen = (x**2 for x in range(10))
# 一度しか使えない
for x in gen:
print(x)
for x in gen:
print(x) # 何も出力されない
# 再利用したい場合
gen = (x**2 for x in range(10))
data = list(gen) # リストに変換(ここで全て実行される!)
for x in data:
print(x) # OK
for x in data:
print(x) # OK
⚠️ 重要:list(gen) の動作
list(gen) を呼ぶと、ジェネレータが最後まで実行され、全ての値がメモリに載ります。
def generator():
print("値1を生成")
yield 1
print("値2を生成")
yield 2
print("値3を生成")
yield 3
gen = generator()
print("リストに変換します")
data = list(gen) # ← ここで全部実行される
print(f"結果: {data}")
出力:
リストに変換します
値1を生成
値2を生成
値3を生成
結果: [1, 2, 3]
つまり:
- ✅ 再利用したいなら
list(gen) - ❌ でも大量のデータは避ける(メモリを使う)
- 💡 本当に再利用が必要か検討する
4. パイプライン処理で繋げる
def read_data(filename):
with open(filename) as f:
for line in f:
yield line
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def parse_timestamp(lines):
for line in lines:
timestamp = line.split()[0]
yield timestamp, line
# パイプライン
pipeline = read_data('app.log')
pipeline = filter_errors(pipeline)
pipeline = parse_timestamp(pipeline)
for ts, line in pipeline:
print(f"{ts}: {line}")
🎓 まとめ
お疲れ様でした!ジェネレータの内部動作、理解できましたね 🎉
復習:ジェネレータが自動でやってくれること
| 機能 | 説明 |
|---|---|
| 🔖 実行位置の記憶 | どこまで実行したか覚えている |
| 💾 変数の保存 | ローカル変数が消えない |
| ⏸️ 一時停止 | yield で停止、next() で再開 |
| 🛑 終了管理 | 自動で StopIteration を送る |
重要ポイント
Part 1: メリット・デメリット
- 🟢 メモリ効率、遅延評価、無限対応、途中終了
- 🔴 一度しか使えない、ランダムアクセス不可、サイズ不明
Part 2: メモリ管理
- 削除タイミング:終了時、
del時、参照消失時 - メモリリーク:無限ジェネレータでリスト蓄積に注意
Part 3: パフォーマンス
- 全部使う場合:ほぼ同じ速度
- 途中で止める場合:ジェネレータが数千倍速い
- ベストプラクティス:大きなデータはジェネレータ、再利用はリスト変換
これでジェネレータの内部動作を深く理解できました! ✨
🚀 次回予告
次回は「イテレータの仕組み」を解説します!
ジェネレータは実は「イテレータ」という仕組みの上に作られています。
次回は、その舞台裏を覗いてみましょう。
- 🔍
__iter__と__next__の秘密 - 🛠️ 自分でイテレータを作る方法
- 🎯
forループが実際に何をしているか
📚 参考資料
🎉 お疲れ様でした!
より深い理解ができましたね。次回のイテレータ編もお楽しみに! 😊