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: yield の基礎

Posted at

📚 対象読者: プログラミングの基礎はあるが、ジェネレータは初めての方
⏱️ 学習時間: 約 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

簡単でしたか?これだけ理解できていれば、実務でジェネレータを使えます!


🚀 次回予告

次回は「イテレータの仕組み」を解説します!

ジェネレータは実は「イテレータ」という仕組みの上に作られています。
次回は、その舞台裏を覗いてみましょう。

🔍 iternext の秘密
🛠️ 自分でイテレータを作る方法
🎯 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?