本当に誰でも読める!! Pythonソースコードの読み方講座(第12回)yield 文とジェネレータ 2

  • HOME
  • 本当に誰でも読める!! Pythonソースコードの読み方講座(第12回)yield 文とジェネレータ 2

本記事は当社が発行しているシミュレーションメールマガジンVol.12の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから

プログラマにもノン・プログラマにも読んでいただきたい「Pythonコードの読み方」講座の第12回です。前回扱った yield を使って複雑な挙動をするプログラムを簡潔に記述する方法を紹介します。 S4のコア実装にも関係するような内容になりますので、シミュレーションに興味がある方にはぜひご一読いただきたいです。

ジェネレータに値を送る

前回、yield XX で関数の途中で処理を止めて値を返すことができる、ということを扱いました。 実は次のコードの func のように yield 文の結果が値を持つように実装することも可能です。

コード1

def func():
    print("値を入れてください(1回目)")
    a = yield "turn-1"
    print(a, "が入力されました。")
    print("値を入れてください(2回目)")
    b = yield "turn-2"
    print(b, "が入力されました。")
    print("合計値:", a+b)
    yield None

gen = func()
res = next(gen)
print(res)
res2 = gen.send(123)
print(res2)
res3 = gen.send(456)

出力1

値を入れてください(1回目)
turn-1
123 が入力されました。
値を入れてください(2回目)
turn-2
456 が入力されました。
合計値: 579

やや複雑ですが、この例は次のように動作します:

  • l.12 : func を初期化して next を呼び出すことで、func 内の処理が最初の yield まで進む。"turn-1" が返り res に代入される
  • l.14 : gen.send(123) とすることで func 内の最初の yield 文の値が 123 と評価されて a に代入される。func 内の処理が2番目の yield 文まで進み、"turn-2" が返り res2 に代入される
  • l.16 : gen.send(456) とすることで func 内の2番目の yield 文の値が 456 と評価されて b に代入される。func 内の処理が最後まで進み、a+b の値が出力されたのち、None が返されて res3 に代入される

新しく、ジェネレータの send メソッドが登場しました。これは「ジェネレータに値を渡しながら動作を再開させる」ための機能です。send の引数として与えたものがジェネレータ内の yield 文の値になります。

シミュレーションでの利用

S4のシミュレーションは Python のジェネレータの機能を使って実装されています。 次の例でそのイメージを紹介します。これは非常に単純なシミュレーションで、利用者が窓口を訪れて、指定した時間だけ利用する、というものです。

コード2

from heapq import heappush, heappop, heapify
def customer(arrival_time, use_time, name):
    start_time = yield (arrival_time, "到着", name)
    done_time = yield (start_time + use_time, "窓口利用", name)

c1 = customer(10, 20, "利用者1")
c2 = customer(15, 10, "利用者2")
dct = {"利用者1": c1, "利用者2": c2}
event = [next(c1), next(c2)]
heapify(event)
while event:
    t, kind, name = heappop(event)
    print(f"{name} が時刻 {t} に {kind} を行いました")
    c = dct[name]
    try:
        e = c.send(t)
        heappush(event, e)
    except StopIteration:
        pass

出力2

利用者1 が時刻 10 に 到着 を行いました
利用者2 が時刻 15 に 到着 を行いました
利用者2 が時刻 25 に 窓口利用 を行いました
利用者1 が時刻 30 に 窓口利用 を行いました

このコードでは大まかには以下のような処理が行われています:

  • l.2-4 : 利用者を表現するジェネレータ関数を定義
  • l.6-8 : ジェネレータオブジェクト(=1人の利用者に相当)を初期化し、利用者名→ ジェネレータオブジェクトを取得するための辞書を定義
  • l.9 : 各利用者の最初のイベントを取得
  • l.10-19 : 時刻順にイベントを処理する。
    • l.12 : イベントをひとつ取り出す
    • l.14 : イベントを起こしたジェネレータオブジェクトを取得
    • l.15-19 : 次のイベントがあればイベントリストに追加する

中でも、以下がジェネレータを使ってシミュレーションを実装する際のポイントになっています:

  • event に時刻をキーにしてイベント(利用者の到着や窓口利用)を管理している
  • 利用者の動作を customer ジェネレータ関数で定義し、customer を呼び出すことで実際の利用者を生成している
  • メインの while ループ文の中で利用者との情報のやり取りにジェネレータの send メソッドを使っている

今回は簡単のため利用者のみをジェネレータオブジェクトとして表現していますが、窓口もジェネレータオブジェクトとして表現すれば、利用状態に応じて利用者の待ち時間が変化する、といったより複雑な現象を再現できるようになります。 このような機能の組み合わせで汎用的なシミュレータは実現されます。

まとめ

以上で2回に渡って Python のジェネレータ機能について紹介しました。

  • 関数定義内に yield 文が含まれている場合、その関数はジェネレータ関数となる
  • ジェネレータオブジェクトを next 関数で動作させると次の yield 文までの処理が実行され、値が返される
  • next の代わりにジェネレータオブジェクトの send メソッドを使うことで処理を再開すると同時に値を渡すことができる

少し複雑な内容になってしまいましたが、コードを読む上では yiled が出てきたら処理の一時停止をしながら呼び出しもとに値のやり取りを行っている、くらいに構えていただければ良いかと思います。

また、これらの機能を組み合わせることで成り立っているS4の内部実装にも思いを馳せていただけると幸いです。

豊岡 祥 株式会社 NTTデータ数理システム シミュレーション&マイニング部所属。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。

著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱
「数理科学の基礎知識」e-book無料ダウンロードはこちら