0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python ジェネレータシリーズ #1.5-理論編: 内部動作を深く理解する

0
Last updated at Posted at 2025-11-08

📚 対象読者: ジェネレータの基本を理解し、より深く学びたい方
⏱️ 学習時間: 約 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 ループが実際に何をしているか

#2 イテレータの仕組み


📚 参考資料


🎉 お疲れ様でした!

より深い理解ができましたね。次回のイテレータ編もお楽しみに! 😊

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?