- HOME
- 本当に誰でも読める!! Pythonソースコードの読み方講座(第12回)yield 文とジェネレータ 2
2023年10月11日 12:23
本記事は当社が発行しているシミュレーションメールマガジンVol.12の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから
- Pythonソースコードの読み方講座(第1回) Pythonコードの構成要素
- Pythonソースコードの読み方講座(第2回) classにまつわるコードの読み方
- Pythonソースコードの読み方講座(第3回) キーワード編 1: 構文に関するキーワード
- Pythonソースコードの読み方講座(第4回) キーワード編 2: 続・構文に関するキーワード
- Pythonソースコードの読み方講座(第5回) 関数にまつわるコードの読み方
- Pythonソースコードの読み方講座(第6回) ソースコードに現れる「単語」を読む
- Pythonソースコードの読み方講座(第7回) キーワード編 3:Pythonの生命線?import文について
- Pythonソースコードの読み方講座(第8回)Python コードを俯瞰する 1:コードの領域分割と実行順序
- Pythonソースコードの読み方講座(第9回)Python コードを俯瞰する 2:コードの領域間の「見え方」について
- Pythonソースコードの読み方講座(第10回)Python コードを俯瞰する 3:関数内の関数について
- Pythonソースコードの読み方講座(第11回)yield 文とジェネレータ
- Pythonソースコードの読み方講座(第12回)yield 文とジェネレータ 2
プログラマにもノン・プログラマにも読んでいただきたい「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の内部実装にも思いを馳せていただけると幸いです。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。
著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱