cover_effective_python

【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻 投稿一覧へ戻る

Tags: Python , generator , send , iterator , yield , Effective

Published 2020年7月3日20:34 by T.Tsuyoshi

さて今回取り上げる話題は前回の ジェネレータの反復作業中に外部から値を注入して出力結果に反映させよう!代入式の右辺に yield 式があったり、send() メソッドを使ったり!の巻 の続き、という位置づけですので、読んでいない方は目を通しておいていただいた方が理解しやすいと思います。


前回は、send() メソッドを利用するとジェネレータの yield 式に値を設定することが可能になり、その値を利用して次の yield 式からの出力値を変更可能、ということを説明しました。


さて今回は、より複雑な波形を出力したい、という「足るを知らない」人間の欲望に基づいてプログラムをアップデートしなければいけなくなったところから話が始まります。


目指すは1周期の正弦波 (サイン波) ではなく、1周期ごとに異なる振幅、ステップ数からなる複数周期の波形データの出力、です。


これを実現するための実装方法の1つは、yield from 式を利用して複数のジェネレータを組み合わせる方法です。
まずは通常の __next__() 特殊関数を呼び出して ( ここでは for 文を利用することでそれを実現しています ) ジェネレータを動作させるバージョンで試してみましょう。


import math


def wave(amplitude, steps):
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):
if output is None:
print(f"Output is None")
else:
print(f"Output: {output:>5.1f}")


def complex_wave():
"""
条件の異なる波形から複数周期のデータを作成します。
"""
yield from wave(7.0, 3)
yield from wave(2.0, 4)
yield from wave(10.0, 5)


def run(it):
for output in it:
transmit(output)


run(complex_wave())
# Output: 0.0
# Output: 6.1
# Output: -6.1
# Output: 0.0
# Output: 2.0
# Output: 0.0
# Output: -2.0
# Output: 0.0
# Output: 9.5
# Output: 5.9
# Output: -5.9
# Output: -9.5



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






素晴らしい! ちゃんと期待通りに動いているようです。
この調子でジェネレータに send() メソッドで値を注入しながら動作させるバージョンもやってみましょう。


def wave_modulating(steps):
step_size = 2 * math.pi / steps
amplitude = yield
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
out_mod = fraction * amplitude
amplitude = yield out_mod


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


def complex_wave_modulating():
yield from wave_modulating(3)
yield from wave_modulating(4)
yield from wave_modulating(5)


def run_modulating(it_mod):
amplitudes = [None, 7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
for amplitude in amplitudes:
out_mod = it_mod.send(amplitude)
transmit_modulating(out_mod)


run_modulating(complex_wave_modulating())
# Output is None
# Output: 0.0
# Output: 6.1
# Output: -6.1
# Output is None
# Output: 0.0
# Output: 2.0
# Output: 0.0
# Output: -10.0
# Output is None
# Output: 0.0
# Output: 9.5
# Output: 5.9



??????????
何か中途半端にはうまくいっているようですが、None が複数回返ってきています、これは何故なんでしょう?


このバージョンでは、ネストされているジェネレータ ( wave_modulating ) が2つの yield 式を含んでいます。


1つ目の yield 式はこのジェネレータの最初の動作で振幅値 ( amplitude ) の初期値を受け取るためのものですが、まずこの yield 式が None を返し、そこでジェネレータは一時停止、次の send() メソッドによる呼び出しで再開、このとき yield 式自体が評価されて amplitude 変数に send() からの値がセットされる、という手順を踏んでいます。


つまり、1つの yield from 式が実行され新しいネストジェネレータが開始される度にこのプロセスが繰り返されて、結果として None が3回出力されているわけです。


興味がある方は動作を追いかけてみてください。
このプログラムが終了したのは yield from 式で呼び出された3つのネストジェネレータの動作が完了したからではなく、run_modulating() における for 文が終了したからだ、ということが分かると思います。None が複数回出力された分ループ回数が足りなくなっちゃったんですね。


この結果から分かることは、yield from 式と send() メソッドによるジェネレータへの値の注入機能を併用するこの実装方法はうまくいかないことがある、ということです。
もちろん、run_modulating() 関数へ処理を追加することでこの None 問題へ対処することは可能ですが、send() メソッドを利用することでただでさえ追いかけづらくなっているコードに、さらに yield from 式を追加し、その上エラー対処コードまで付け加えることにあまり意味があるとは思えません。


おススメは、可能な限り send() メソッドは使わずに、よりシンプルな実装方法で処理をする、ことです。


考えられる簡単な解決方法の1つは、wave() 関数に、計算に適用して欲しい振幅値を提供するためのイテレータを渡し、各ステップの計算ループ内で next() を利用してそのイテレータからその都度振幅値を取り出す、というものです。


def wave_cascading(amplitude_it, steps):
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
amplitude = next(amplitude_it) # ここでその都度振幅値を受け取ります
output = amplitude * fraction
yield output



この wave_cascading() ジェネレータをネストさせて yield from 式で複数回呼び出す場合でも、amplitude_it パラメータに渡すイテレータは同じものを指定できます。
プログラムを通してイテレータは stateful、つまり前回何処まで実行されたかを記憶していますから、それぞれのネストジェネレータがこのイテレータから受け取る値は前のネストジェネレータが受け取った次の値から、ということが保証されていますからね。


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


def complex_wave_cascading(amplitude_it):
yield from wave_cascading(amplitude_it, 3)
yield from wave_cascading(amplitude_it, 4)
yield from wave_cascading(amplitude_it, 5)


def run_cascading():
amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
it = complex_wave_cascading(iter(amplitudes))
for output in it:
transmit_cascading(output)


run_cascading()
# Output: 0.0
# Output: 6.1
# Output: -6.1
# Output: 0.0
# Output: 2.0
# Output: 0.0
# Output: -2.0
# Output: 0.0
# Output: 9.5
# Output: 5.9
# Output: -5.9
# Output: -9.5



この実装方法の最大の長所は、振幅値を生成するイテレータはこの例のようにあらかじめ用意されているもの (ここではリスト) である必要はなく、ジェネレータ関数などを利用するまったく動的なものであってもOK、という自由度の高さだと思います。


まとめ:

1: send() メソッドを利用して、yield 式自体の値という形でジェネレータに値を注入することができます。

2: send() メソッドと yield from 式の併用は思わぬ結果を生じる場合がありますから、可能な限り避けましょう。

3: send() メソッドの使用を考える前に、ジェネレータに注入する値を提供するイテレータをネストされているジェネレータに順送りで渡していく方法を検討しましょう。

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

0 comments

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

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