cover_effective_python

【 Effective Python, 2nd Edition 】ジェネレータの反復動作中に外部から値を注入して出力結果に反映させよう! 代入式の右辺に yield 式があったり、send() メソッドを使ったり!の巻 投稿一覧へ戻る

Tags: Python , generator , send , yield , Effective

Published 2020年7月2日21:51 by T.Tsuyoshi

yield 式を含むジェネレータ関数 ( generator function ) はシーケンスデータの値を1回に1つずつ提供してくれますが、このデータのやり取りはあくまでも一方通行です。


もし、ジェネレータ関数が反復作業をしている最中に、yield 式が出力する値に何らかの変更を加えるようなデータをこちら側から与えて、それに伴って出力の値が変化するような双方向コミュニケーションが取れたらなかなか面白いと思いませんか?


例えば、サイン波1周期を指定されたステップ数で分割した際のそれぞれの時点のおおよそのの値を出力するプログラムを作成するとします。

import math


def wave(amplitude, steps):
"""
amplitude: 振幅
steps: ステップ数

各ステップ時点での弧度 ( radians ) を求め、それに対応するサイン値 ( fraction ) を計算します。
その値に振幅 ( amplitude ) を掛けたものが求める値になります。
"""
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
output = fraction * amplitude
yield output


def transmit(output):
"""
yield 式の出力値を表示する表示関数です
"""
if output is None:
print(f"Output is None")
else:
print(f"Output: {output:>5.1f}")


def run(it):
"""
yield 式から計算結果を受け取り表示関数へ渡します
"""
for output in it:
transmit(output)


run(wave(3.0, 8))
# Output: 0.0
# Output: 2.1
# Output: 3.0
# Output: 2.1
# Output: 0.0
# Output: -2.1
# Output: -3.0
# Output: -2.1



グラフで表すとこんな感じです。






これはこれでちゃんと機能していますが、yield 式が値を反復出力している途中で振幅値を変化させて、それに応じた計算値を取得したいと思います。
そのためには、ジェネレータ関数 ( wave() ) の各実行時にこちら側から値を渡す手段が必要です。


ここで登場するのが send() メソッドです。
send() メソッドを利用することでジェネレータ関数実行中にこちら側から値を渡せる、ある意味「双方向コミュニケーションジェネレータ」を記述することができるようになります。


ただ、慌てちゃいけません、ここでちょっと寄り道して準備体操です。


ジェネレータが反復作業している時点の yield 式の値は通常 None です。
何を言っているか訳分かりませんね。頑張って説明してみたいと思います。頑張って理解してください。


def my_generator():
for i in range(2):
received = yield i #3, #6
print(f"yield 式の評価値: {received}") #7


it = iter(my_generator()) #1
output = next(it) #2
print(f"出力: {output}") #4
# 出力: 0


output = next(it) #5
print(f"出力: {output}")
# yield 式の評価値: None #8
# 出力: 1


try:
output = next(it)
print(f"出力: {output}")
except StopIteration:
print("終了")
# yield 式の評価値: None
# 終了



流れを追ってみます。

1: ジェネレータイテレータを返します。実際の動作は行いません。

2: next() によりジェネレータメソッドが開始されて、最初の yield 式まで実行されます。

3: yield i まで実行されて i を返し動作を停止します。

4: 結果を出力します。

5: 次の next() によりジェネレータメソッドが再開します。

6: 前回停止した次の式 received = yield i が実行されて、この時点の yield 式の値が received 変数にセットされます。

7: 6 の値を出力します。

8: None ですね。




ですから、ジェネレータの再開が for 文や next() を利用した __next__() によるものの場合、その時点での yield 式自体の値は None である、ということです。


さて、お待たせしました。ここで send() メソッドの登場です。
next() メソッドの代わりに send() メソッドに引数を渡してジェネレータを動かすことで、ジェネレータが再開した時点での yield 式自体の値がその引数の値になるんです。


ですが、もう1回、上の例の流れをちゃんと追ってください。
yield 式自体が評価された値が変数 ( received ) に代入されているのは何回目に next() を呼び出したときでしょうか?


最初の next() の実行時 (#2) では yield 式が値を返してきているだけで式自体が評価されてはいません。
2回目の next() の実行 (#5) で初めて yield 式自体が評価されて変数 ( received ) に値が代入されています。


つまり、send() メソッドの利用時にもまったく同じことが言えますので、yield 式に値を渡せるのは2回目以降の send() メソッド呼び出しから、ということになります。
そして、最初の send() メソッド呼び出し時には yield 式自体の評価までコードが進行しませんから、必ず None を渡さなければいけません
None 以外の値を渡した場合は実行時例外 ( ランタイムエラー ) が発生します。


def my_generator():
for i in range(2):
received = yield i
print(f"yield 式の評価値: {received}")


it = iter(my_generator())
output = it.send(None) # 1 回目の呼び出しでは必ず None を渡さなければエラーになります
print(f"出力: {output}")
# 出力: 0


output = it.send('2 回目の send() での呼び出しです')
print(f"出力: {output}")
# yield 式の評価値: 2 回目の send() での呼び出しです
# 出力: 1


try:
output = it.send('3 回目の send() での呼び出しです')
print(f"出力: {output}")
except StopIteration:
print("終了")
# yield 式の評価値: 3 回目の send() での呼び出しです
# 終了



さて、これでジェネレータの動作途中にこちら側から値を渡す方法が分かりました。
これを利用して、動作途中で異なる振幅値 ( amplitude ) で計算をするようにプログラムを変更してみましょう。


def wave_modulating(steps):
step_size = 2 * math.pi / steps
amplitude = yield # ここで最初の amplitude を受け取ります
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
out_mod = fraction * amplitude
amplitude = yield out_mod # 2 回目以降の amplitude はここでその都度受け取ります



結果を表示する関数には変更はありません。


def transmit_modulating(out_mod):
if out_mod is None:
print(f"Output is None")
else:
print(f"Output: {out_mod:>5.1f}")



あと、ジェネレータを起動・再開させる run() 関数も、ジェネレータを動かすごとに amplitude の値を yield 式へ渡すように変更する必要があります。


def run_modulating(it_mod):
amplitudes = [None, 3, 5, 8, 11, 8, 5, 3, 5, 8, 11, 8, 5]
for amplitude in amplitudes:
out_mod = it_mod.send(amplitude)
transmit_modulating(out_mod)


run_modulating(wave_modulating(12))
# Output is None
# Output: 0.0
# Output: 2.5
# Output: 6.9
# Output: 11.0
# Output: 6.9
# Output: 2.5
# Output: 0.0
# Output: -2.5
# Output: -6.9
# Output: -11.0
# Output: -6.9
# Output: -2.5



グラフで表すとこんな感じです。






ちゃんと機能しています。
最初の出力は None で、これも予想通りです。この時点では amplitude はセットされていません。
さきほどもお話した通り、send() による値が yield にセットされるのは2回目の呼び出し以降ですからね。


このコードでは、yield 式が代入式の右辺にあったり、イテレータを進めているのが引数が設定されている send() メソッドだったり、と
普段使いではちょっと目にしない記述があって、少し読み解くのが難しくなっているのが難点です。


ただ、工夫次第では非常に面白い使い道がありそうなプログラムではあります。
が、send() を使用したジェネレータへの値の注入にはかなり注意が必要なんです。


長くなってしまいましたので今回はここまでで終わりにしたいと思いますが、
次回は、send() メソッド使用時の注意点、そして、
できれば send() メソッドを使用したジェネレータへの値の注入はやらない方が良い、という
ここまで読んでくださった方へ冷や水を浴びせるような内容となる予定です。


お楽しみに! って、楽しみになんか待てるわけないですね、こんな予告じゃ。

つづき をどうぞ。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)