Pythonにおけるジェネレーター

Pythonにおけるジェネレーター

この記事ではPythonにおけるジェネレーターについて説明します。

YouTube Video

Pythonにおけるジェネレーター

概要

Pythonの**ジェネレーター(generator)**は、イテレーターの一種で、繰り返し処理を効率的に行うための強力な機能です。大量のデータを扱う際に、メモリ効率の良いコードを記述できるようになります。

ジェネレーターとは?

Pythonのジェネレーターは、値を一度に1つずつ生成する特殊な関数で、yield キーワードを使って定義されます。関数の実行状態を保持したまま一時停止し、次回再開できるのが特徴です。

yield の基本

yield は、関数の途中で値を返しつつ実行を一時停止するためのキーワードです。

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • この関数は、呼び出すと値を順番に返すジェネレーターオブジェクトを返します。
  • 次の値がない状態で next() を呼ぶと、StopIterationエラーが発生します。

next()StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • このように、明示的にStopIterationエラーを処理することで、ジェネレーターの終了を検知できます。

send(value)

send(value)を呼ぶと、ジェネレーターを再開させ、yield式の位置にvalueを渡せます。渡した値は yield 式の戻り値としてジェネレーター側で受け取れます。初回は、send(value)None 以外を送れないため、next() または send(None) を使う必要があります。

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • send(10) によって、ジェネレーター側のyield10 を返す式になり、xに10が代入されます。

throw()

throwを呼び出すと、ジェネレーターを再開しつつ、停止中の yield の位置で例外を発生させることができます。ジェネレーター内で例外を処理することで処理を継続できます。捕捉しない場合は外に例外が伝播し、ジェネレーターは終了します。

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • このコードでは、throwを呼び出して、ジェネレーターに例外を注入しています。ジェネレーター側では、例外処理をして recovered を返しています。

close()

close()を呼び出すと、ジェネレーターを終了させます。ジェネレーター内部では、finallyでクリーンアップができます。close()を呼び出した後にnext()send()を呼ぶとStopIterationエラーが発生します。

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • このコードは、close()でジェネレーターを終了させ、finallyのクリーンアップ処理が実行されることを示します。

yield from

yield fromサブジェネレーターの委譲 を行うための構文です。ジェネレーターの中で別のジェネレーターを呼び出し、すべての値をそのまま外側に流すためのシンプルな書き方です。

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • このコードは、yield from でサブジェネレーターの全ての値を外側に委譲して流し、その後に3yieldしています。

イテレーターとの関係

ジェネレーターは __iter__()__next__() を内部的に持ち、イテレーターの一種です。そのため、for 文などのイテラブルな処理と完全に互換性があります。

for ループとの連携

Pythonでは、for ループが内部的に next() を使って値を自動で取得します。

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

この方法なら StopIteration の処理も自動です。

無限ジェネレーターの作成

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

無限ループを作成することも可能ですが、使い方には注意が必要です。

ジェネレーター式(Generator Expression)

丸括弧を用いたジェネレーター式によって、リスト内包表記のような構文で、ジェネレーターを簡単に記述できます。

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

リスト内包表記と異なり全要素を一度にメモリに展開しないため、メモリ効率が良くなります。

ジェネレーターのエラーハンドリング

ジェネレーター内では例外が発生する可能性があります。その場合は、通常のPythonコードと同様にtry-exceptを使用します。

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

この例ではゼロ除算が発生した場合に、適切なエラーハンドリングを実行しています。

ジェネレーターのスタックトレース

ジェネレーター内で例外が起きた場合、その例外はジェネレーターが再開したときに発生します。

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • このジェネレータは、最初に1を返します。再開時に発生したエラーは捕捉され、エラーメッセージとして表示されます。

ジェネレーターの活用例

ファイルの1行ずつ読み込み(大規模ファイル対応)

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • この関数はテキストファイルをイテレータで1行ずつ読み込み、各行の前後空白を除去して返すジェネレーターのため、大規模ファイルでも低メモリで処理できます。

Fibonacci数列のジェネレーター

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • このコードは、ジェネレーターで上限値未満のフィボナッチ数を順に生成し、for ループでそれらの数値を出力します。

活用方法

他にも次のような場面でジェネレーターを活用できます。

  • 巨大なCSVやログファイルの逐次処理
  • APIのページネーション
  • ストリーミングデータの処理(例: Kafka, IoTデバイス)

まとめ

概念 ポイント
yield 一時停止して値を返す
ジェネレーター関数 yield を含む関数で、呼び出すとイテレーターになる
メリット メモリ効率が良い、大規模データの処理に最適
ジェネレーター式 (x for x in iterable) のような簡潔な記述が可能

ジェネレータを利用することで、メモリを効率的に利用しながら、大規模データを処理でき、コードも簡潔に保つことができます。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video