【Python】ジェネレーターとジェネレーター式の使い方

【Python】ジェネレーターとジェネレーター式の使い方

目次

Pythonを学んでいると「ジェネレーター」という概念に出会うことがありますが、最初はその仕組みや使い方がわかりにくいかもしれません。この記事では、ジェネレーターとジェネレーター式について、初心者の方にもわかりやすく解説していきます。

ジェネレーターとは?

通常の関数との違い

まずは通常の関数とジェネレーターの違いを見てみましょう。

# 通常の関数
def get_squares_normal(n):
    result = []
    for i in range(n):
        result.append(2 ** i)
    return result

# ジェネレーター
def get_squares_generator(n):
    for i in range(n):
        yield 2 ** i

見た目はほとんど同じですが、returnの代わりにyieldを使っているのがポイントです。

動作の確認

それぞれの関数がどのような動作をするか確認してみましょう。

# 通常の関数
def get_squares_normal(n):
    result = []
    for i in range(n):
        result.append(2 ** i)
    return result

squares = get_squares_normal(5)
print(type(squares)) # <class 'list'>
print(squares) # [1, 2, 4, 8, 16]

一方、ジェネレーターの場合は:

def get_squares_generator(n):
    for i in range(n):
        yield 2 ** i

squares = get_squares_generator(5)

print(type(squares))  # <class 'generator'>
print(squares)  # <generator object ...>

# for文で値を取り出す
for square in squares:
    print(square, end=' ')  # 1, 2, 4, 8, 16

print()  # 改行

通常の関数(return文)は戻り値として指定された結果を返し処理を終了しますが、ジェネレーター(yield文)は実行時点の値を返し、一時停止状態になって次の呼び出しを待ちます。

ジェネレーターの特徴

ジェネレーターには以下のような特徴があります:

  • 見た目は通常の関数とほぼ同じ
  • return文の代わりにyield文を使う
  • イテレーターを返す(for文で値を取り出せる)
  • 値を「必要な時に」「1つずつ」生成する特殊な関数

ジェネレーター式の使い方

リスト内包表記と似た書き方で、より簡潔にジェネレーターを作ることもできます。

# リスト内包表記:[ ] を使う
squares_list = [2**i for i in range(10)]
print(type(squares_list))  # <class 'list'>

# ジェネレーター式:( ) を使う
squares_gen = (2**i for i in range(10))
print(type(squares_gen))   # <class 'generator'>

# 使い方は同じ
for num in squares_gen:
    print(num, end=' ')  # 1 2 4 8 16 32 64 128 256 512

print()  # 改行

角括弧[]の代わりに丸括弧()を使うだけで、簡単にジェネレーターが作れます。

なぜジェネレーターを使うのか?

ジェネレーターを使う主なメリットは以下の3つです:

  • メモリ効率が良い
  • 処理が速い(初期化が速い)
  • 無限のデータも扱える

メモリ使用量の比較

実際にメモリ使用量を比較してみましょう。

import sys

# リスト
big_list = [_ for _ in range(1000)]
print(f"リストのサイズ: {sys.getsizeof(big_list)} bytes")

# ジェネレーター
big_gen = (_ for _ in range(1000))
print(f"ジェネレーターのサイズ: {sys.getsizeof(big_gen)} bytes")

実行結果

リストのサイズ: 8856 bytes
ジェネレーターのサイズ: 192 bytes

実行すると、ジェネレーターの方が圧倒的に少ないメモリしか使用していないことがわかります。

パフォーマンスの比較

処理速度も比較してみます。

import time

def timer(func):
    start = time.time()
    result = func()
    end = time.time()
    return result, end - start

# リスト版
def with_list():
    return [i**2 for i in range(1000000)]

# ジェネレーター版
def with_generator():
    return (i**2 for i in range(1000000))

_, list_time = timer(with_list)
_, gen_time = timer(with_generator)

print(f"リスト: {list_time:.7f}秒")
print(f"ジェネレーター: {gen_time:.7f}秒")

実行結果

リスト: 0.0734010秒
ジェネレーター: 0.0000057秒

ジェネレーターは実際に値を生成するまで計算を行わないため、初期化が非常に高速です。

無限データの扱い

ジェネレーターを使えば、無限に続くデータシーケンスも扱えます。

def get_squares_generator():
    num = 1
    while True:
        yield num
        num *= 2

squares = get_squares_generator()

# next()関数で1つずつ値を取り出す
print(next(squares))  # 1
print(next(squares))  # 2
print(next(squares))  # 4
print(next(squares))  # 8
print(next(squares))  # 16

next()関数を使うことで、イテレーターから次の値を1つずつ取り出すことができます。

実践的な使用例

大きなファイルの読み込み

大容量のファイルを扱う際に、ジェネレーターは威力を発揮します。

例えば以下のようなログファイルからエラー情報を抜き出したい場合、

2024-03-15 10:23:45 INFO: アプリケーションを起動しました
2024-03-15 10:23:46 INFO: データベースに接続中...
2024-03-15 10:23:47 INFO: データベース接続成功
2024-03-15 10:23:48 INFO: ユーザー認証モジュールを初期化
2024-03-15 10:23:49 INFO: キャッシュシステムを起動
2024-03-15 10:24:01 INFO: ユーザー[user001]がログインしました
2024-03-15 10:24:05 INFO: ユーザー[user001]がダッシュボードにアクセス
2024-03-15 10:24:12 WARNING: キャッシュメモリ使用率が70%を超えました
2024-03-15 10:24:15 INFO: ユーザー[user002]がログインしました
2024-03-15 10:24:18 ERROR: データベース接続がタイムアウトしました (接続ID: db_conn_045)
2024-03-15 10:24:19 INFO: データベース再接続を試行中...
2024-03-15 10:24:20 INFO: データベース再接続成功
2024-03-15 10:24:25 INFO: ユーザー[user003]がログインしました
2024-03-15 10:24:30 INFO: ユーザー[user002]がファイルをアップロード (file_id: doc_12345.pdf)
2024-03-15 10:24:35 WARNING: ディスク容量が残り20%です
2024-03-15 10:24:40 INFO: 定期バックアップを開始
2024-03-15 10:24:45 INFO: バックアップ完了 (backup_id: bk_20240315_102445)
2024-03-15 10:24:50 ERROR: ユーザー[user004]の認証に失敗しました - パスワードが正しくありません
2024-03-15 10:24:52 INFO: ユーザー[user004]のアカウントを一時的にロック
2024-03-15 10:24:55 INFO: ユーザー[user001]がログアウトしました
2024-03-15 10:25:00 INFO: システムヘルスチェック開始
2024-03-15 10:25:01 INFO: CPU使用率: 45%
2024-03-15 10:25:02 INFO: メモリ使用率: 62%
2024-03-15 10:25:03 INFO: ディスク使用率: 80%
2024-03-15 10:25:05 WARNING: ディスク使用率が高くなっています
2024-03-15 10:25:10 ERROR: 外部APIへの接続に失敗しました (api.example.com - タイムアウト)
2024-03-15 10:25:11 INFO: 外部API接続をリトライ中... (1/3)
2024-03-15 10:25:15 INFO: 外部API接続成功
2024-03-15 10:25:20 INFO: ユーザー[user005]がログインしました
2024-03-15 10:25:25 INFO: ユーザー[user003]がレポートを生成 (report_id: rpt_2024_Q1)
2024-03-15 10:25:30 INFO: メール通知を送信 (recipient: admin@example.com)
2024-03-15 10:25:35 ERROR: メール送信に失敗しました - SMTPサーバーが応答しません
2024-03-15 10:25:36 INFO: メール送信をキューに追加
2024-03-15 10:25:40 INFO: ユーザー[user002]がプロフィールを更新
2024-03-15 10:25:45 INFO: セッションクリーンアップを実行
2024-03-15 10:25:46 INFO: 期限切れセッション5件を削除
2024-03-15 10:25:50 WARNING: 不正なリクエストを検出 (IP: 192.168.1.100)
2024-03-15 10:25:51 INFO: IPアドレス 192.168.1.100 を一時的にブロック
2024-03-15 10:25:55 INFO: ユーザー[user006]がログインしました
2024-03-15 10:26:00 INFO: データベースの自動バキューム開始
2024-03-15 10:26:05 INFO: バキューム完了
2024-03-15 10:26:10 ERROR: ファイルアップロードに失敗 - ファイルサイズが制限を超えています (user: user003, size: 512MB)
2024-03-15 10:26:15 INFO: ユーザー[user005]がドキュメントを共有 (doc_id: doc_67890)
2024-03-15 10:26:20 INFO: システムアップデートの確認中...
2024-03-15 10:26:21 INFO: 新しいアップデートが利用可能です (version: 2.5.1)
2024-03-15 10:26:25 INFO: ユーザー[user002]がログアウトしました
2024-03-15 10:26:30 WARNING: APIレート制限に近づいています (使用率: 90%)
2024-03-15 10:26:35 INFO: キャッシュをクリア
2024-03-15 10:26:40 ERROR: データ同期エラー - リモートサーバーとの不整合を検出
2024-03-15 10:26:41 INFO: データ同期を再実行中...
2024-03-15 10:26:45 INFO: データ同期完了
2024-03-15 10:26:50 INFO: ユーザー[user007]がログインしました
2024-03-15 10:26:55 INFO: 定期レポートを生成中...
2024-03-15 10:27:00 INFO: レポート生成完了
2024-03-15 10:27:05 INFO: システムパフォーマンス最適化を実行
2024-03-15 10:27:10 ERROR: プラグインの読み込みに失敗 (plugin: analytics_v2)
2024-03-15 10:27:11 WARNING: プラグインなしで続行します
2024-03-15 10:27:15 INFO: ユーザー[user003]がログアウトしました
2024-03-15 10:27:20 INFO: バックグラウンドタスク完了 (task_id: bg_task_789)
2024-03-15 10:27:25 INFO: システムステータス: 正常
2024-03-15 10:27:30 INFO: 次回のメンテナンスまで: 72時間
def read_large_file(file_path):
    """大きなファイルを1行ずつ読む"""
    with open(file_path, 'r') as file:
        for line in file:
            yield line.strip()

# メモリを節約しながら処理
for line in read_large_file('data.txt'):
    if 'error' in line.lower():
        print(f"エラー発見: {line}")

実行結果

エラー発見: 2024-03-15 10:24:18 ERROR: データベース接続がタイムアウトしました (接続ID: db_conn_045)
エラー発見: 2024-03-15 10:24:50 ERROR: ユーザー[user004]の認証に失敗しました - パスワードが正しくありません
エラー発見: 2024-03-15 10:25:10 ERROR: 外部APIへの接続に失敗しました (api.example.com - タイムアウト)
エラー発見: 2024-03-15 10:25:35 ERROR: メール送信に失敗しました - SMTPサーバーが応答しません
エラー発見: 2024-03-15 10:26:10 ERROR: ファイルアップロードに失敗 - ファイルサイズが制限を超えています (user: user003, size: 512MB)
エラー発見: 2024-03-15 10:26:40 ERROR: データ同期エラー - リモートサーバーとの不整合を検出
エラー発見: 2024-03-15 10:27:10 ERROR: プラグインの読み込みに失敗 (plugin: analytics_v2)

ファイル全体をメモリに読み込むことなく、1行ずつ処理できます。

無限数列の生成

数学的な数列を無限に生成することもできます。

def fibonacci():
    """フィボナッチ数列を無限に生成"""
    a, b = 0, 1
    while True:  # 無限ループ
        yield a
        a, b = b, a + b

# 必要な分だけ取得
fib = fibonacci()
for _ in range(10):
    print(next(fib), end=' ')
# 0 1 1 2 3 5 8 13 21 34

print()  # 改行

データ処理パイプライン

複数のジェネレーターを組み合わせて、効率的なデータ処理パイプラインを構築できます。

def read_data():
    """データを読み込む"""
    for i in range(1, 11):
        yield f"data_{i}"

def process_data(data_gen):
    """データを処理"""
    for data in data_gen:
        yield data.upper()

def filter_data(data_gen):
    """条件でフィルタリング"""
    for data in data_gen:
        if '5' not in data:
            yield data

# パイプライン実行
pipeline = filter_data(process_data(read_data()))
for result in pipeline:
    print(result)  # DATA_1, DATA_2, DATA_3, DATA_4, DATA_6...

ジェネレーターの利用シーン

使うべき場面

ジェネレーターは以下のような場面で威力を発揮します:

  • 大量のデータを扱うとき
  • メモリを節約したいとき
  • 無限のデータストリームを扱うとき
  • データを1つずつ順次処理するとき

使わない方がいい(使えない)場面

一方で、以下の場面ではジェネレーターは適していません:

  • データを何度も使い回すとき
  • ランダムアクセスが必要なとき
  • データ全体の長さを知る必要があるとき

ジェネレーターは一度使用すると「使い切り」になってしまうため、繰り返し使用する場合は毎回新しいジェネレーターを作成する必要があります。

まとめ

ジェネレーターは、Pythonでメモリ効率的なプログラムを書くための強力なツールです。以下のポイントを覚えておきましょう:

  1. yieldでジェネレーター関数を作る
  2. ()でジェネレーター式を作る
  3. メモリ効率が良い
  4. 1回きりの使用(再利用不可)
  5. 遅延評価で必要な時に計算

特に大量のデータを扱う際や、メモリ使用量を抑えたい場面では、ジェネレーターの活用を検討してみてください。最初は少し理解しにくいかもしれませんが、使いこなせるようになると、より効率的なPythonプログラムが書けるようになります。