コンテンツ

Pythonインタラクティブガイド - ステップ3 関数 (10) - ジェネレータ関数

シリーズ - Pythonインタラクティブガイド
Info
  • 本講座「Pythonインタラクティブガイド」は、手を動かしながらPythonプログラミングの基礎を学べる実践的な講座です。
  • 「スタイルガイド」では、Pythonで読みやすくきれいなコードを書くためのガイドライン(PEP8)を主に紹介しています。
  • 各コード例はその場で実行して結果を確認できます。
    ページ再読み込みで元に戻るので、自由に試してみてください。

「ステップ3 関数」の続きです。

今回は、データを効率的に扱うための特殊な関数であるジェネレータ関数について学んでいきます。

「2.2.3. 内包表記」では、ジェネレータ式について扱いました。
ジェネレータ式は、以下の性質を持つ「ジェネレータ(オブジェクト)」を返します:

  • イテラブル(繰り返し可能)である
  • 値を一度だけ順に取り出せる「使い切り」のオブジェクト
  • 値をすべて一度に生成するのではなく、必要に応じて一つずつ生成するためメモリ効率が良い

今回扱う ジェネレータ関数 も、同様にジェネレータを返す関数です。

ジェネレータ関数の基本構文
def ジェネレータ関数名(): 処理 yield 値1 処理 yield 値2 ...
  • 上記ジェネレータ関数で生成されたジェネレータは、コードを順に実行し、yield文に到達するとその時点で指定された値を返します。 この動作を関数が終了するまで繰り返します。
  • 関数が終了すると値の生成も終了します。
  • next()関数を用いると、ジェネレータから次の値を1つずつ取得できます。
  • すべての値を生成後にnext()を呼び出すと、StopIteration例外が発生します。

上記の例から分かるように、ジェネレータ関数が出力するジェネレータは以下の特徴を持ちます:

  1. next()関数を呼び出すと、最初のyield文までコードを実行して値を返す
  2. 次にnext()が呼ばれると、前回のyield文の直後から実行が再開され、次のyield文までコードを実行して値を返す
  3. 上記2パターンのいずれの場合も、次のyield文が存在しない場合は、コードを最後まで実行した後に StopIteration 例外を発生させる

実はfor文は内部で「next()関数を繰り返し呼び出し、StopIterationが発生したら処理を終了する」という仕組みで動いています。

📚練習問題

引数nを受け取り、0 ~ nの偶数を順番に生成するジェネレータ関数 even_numbers を作成しましょう。

解答例
📚練習問題

再帰関数の項でも扱った、「コラッツ数列」を生成するジェネレータを作成しましょう。

コラッツ数列とは、入力された正の整数 n から始めて、以下のルールに従って 1 に到達するまで続ける数列 のことです。

  • n が 1 の場合: 処理を終了する
  • n が偶数の場合: 次の数は n / 2
  • n が奇数の場合: 次の数は 3n + 1

この手順を繰り返すと、任意の数で最終的に1に到達すると予想されています(「コラッツ予想」)。

(: n=6 の場合、 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1)

引数 n を受け取り、コラッツ数列を順に生成するジェネレータ collatz_generator を実装しましょう。

解答例

ジェネレータ関数を使用すると、リストでは表現できないような無限に続く数列も生成できます。

無限ジェネレータをforループで使用する場合は、何らかの条件で中断する必要があります:

yield from構文を使用すると、イテラブル(リストやジェネレータなど)から値を順に生成できます。
これを用いて、複数のジェネレータを組み合わせたジェネレータの作成が簡潔に行えます:

yield from構文
yield from イテラブル

yield fromを使わない場合、以下のように冗長な記述が必要になります:

yield fromは単に短縮構文というだけでなく、後に紹介するsendthrowcloseといった操作も内部のジェネレータへそのまま引き渡す働きを持っています。

📚練習問題

yield fromを使って、以下の条件を満たすジェネレータ関数 combined_sequence を作成しましょう:

  1. 引数として3つの異なる数値リストを受け取る
  2. それぞれのリストの要素を順番に生成する
  3. 各リストの間に区切り値として-1を生成する
解答例

ここまではジェネレータを主に「値を生成するオブジェクト」として使用してきました。 次に紹介する機能を用いると、外部からジェネレータにデータを送信でき、処理の流れを双方向で制御できるようになります。

send メソッドを使用すると、ジェネレータに値を送信することができます。
送信された値は、以下のように yield 式の戻り値として受け取ります:

sendメソッドで送信した値を受信する
変数 = yield 出力値

上記文では、以下の2つの操作を行っています:

  1. この文に到達すると、「出力値」を出力して実行を一時停止する
  2. この文で停止時にsendメソッドが実行されると、その引数を「変数」に代入して、次のyield文までコードを実行する

上記のようにしてsendメソッドの値を受け取るため、以下に注意してください:

  • sendメソッドを使用するには、そのジェネレータがyield文で停止している必要があります。
    そのため、最初にsendメソッドを使用する前に、next()関数を用いてyield文まで進めておく必要があります。
  • 上記文で停止時にnext()関数が使用されると、「変数」にはNoneが代入されます。
📚練習問題

「受け取った値の統計値を画面出力するジェネレータ」を生成する関数 stats() を作成しましょう。

  • 数値を send メソッドで受け取ります
  • 受け取るたびに、これまで受け取った値の合計・個数・平均を画面出力します
解答例
📚練習問題

ログ監視を行うジェネレータ関数 error_monitor() を作成しましょう。

  • 文字列を send メソッドで受け取ります
  • "ERROR"3 回連続 で現れたら "ALERT" を返してください
  • それ以外は None を返してください
解答例

sendメソッドが通常のデータを送信するのに対し、throwメソッドを用いるとジェネレータに例外を送ることができます。
throwメソッドを呼び出すと、ジェネレータが最後に停止したyield文の位置で指定した例外が発生します。

例外を送信することで、ジェネレータの実行フローを制御できます。
これはジェネレータ内でエラー処理やリソースのクリーンアップなどを行う際に役立ちます。
(エラー処理については後ほど詳しく解説します)

closeメソッドを使用すると、ジェネレータを明示的に終了できます:

closeメソッドは、ジェネレータ内部で使用しているリソースの解放が必要な場合などに使用されます。

今回はジェネレータ関数について学びました。

  • ジェネレータ関数は、yield文を使ってデータを一つずつ生成する特殊な関数です

  • メモリ効率が良く、大量のデータや無限のデータ列でも効率的に扱えます

  • ジェネレータの主な特徴:

    • 値を一度だけ順に取り出せる「使い切り」のオブジェクト
    • 必要に応じて値を一つずつ生成する「遅延評価」方式
    • for文やnext()関数で値を取り出せる
  • 高度な機能

    • yield from:他のイテラブルから値を生成する簡潔な方法
    • send()メソッド:ジェネレータに値を送信して双方向通信を実現
    • throw()メソッド:ジェネレータに例外を送出して制御
    • close()メソッド:ジェネレータを明示的に終了

ジェネレータ関数は、大規模データの処理やストリーム処理など、様々な場面で活用できる強力な機能です。
特に、メモリ使用量を抑えながら大量のデータを扱う必要がある場合に便利です。

関連記事