
【 Effective Python, 2nd Edition 】yield from ステートメントでネストしたジェネレータ ( nested generators, composed generators ) を効率よく処理しよう! 投稿一覧へ戻る
Published 2020年7月1日12:32 by T.Tsuyoshi
ジェネレータを利用することでメモリ消費を抑えることができたり、イテラブル可能な独自クラスを簡単に実装できたり、と多くの利点を享受することができます。
今回は、そんなジェネレータがネストしている場合の効率的な実行方法についてです。
さて、インターバルトレーニング用のプログラムを作りたいと思っています。
最初は速いペースで、ちょっと休んで、次はゆっくりとしたペースで、という指示を出せるようにしたいんです。
そこで、次のような2つのジェネレータを用意しました。
def move(period, speed):
"""ある期間(period)におけるトレーニングスピード(speed)を指示します"""
for _ in range(period):
yield speed
def stop(period):
"""ある期間(period)クールダウンします"""
for _ in range(period):
yield 0
"""ある期間(period)におけるトレーニングスピード(speed)を指示します"""
for _ in range(period):
yield speed
def stop(period):
"""ある期間(period)クールダウンします"""
for _ in range(period):
yield 0
実際のトレーニングプログラムを作成するときには、これら2つのジェネレータを組み合わせて一連の流れを組み立てます。
各インターバルセクション毎にそれぞれのジェネレータを実行し、結果的に1つのトレーニングプログラムを作成します。
def training():
"""2つのジェネレータ (move, stop) を組み合わせて実際のトレーニングプログラムを作ります"""
for section in move(4, 5.0):
yield section
for section in stop(3):
yield section
for section in move(2, 3.0):
yield section
def print_section(section):
"""指示を出力します"""
print(f"Section: {section:.1f}")
def run(func):
"""1つのジェネレータ (func -> training) からの指示に合わせてトレーニングしていきましょう"""
for section in func():
print_section(section)
# トレーニング開始
run(training)
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 0.0
# Section: 0.0
# Section: 0.0
# Section: 3.0
# Section: 3.0
"""2つのジェネレータ (move, stop) を組み合わせて実際のトレーニングプログラムを作ります"""
for section in move(4, 5.0):
yield section
for section in stop(3):
yield section
for section in move(2, 3.0):
yield section
def print_section(section):
"""指示を出力します"""
print(f"Section: {section:.1f}")
def run(func):
"""1つのジェネレータ (func -> training) からの指示に合わせてトレーニングしていきましょう"""
for section in func():
print_section(section)
# トレーニング開始
run(training)
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 0.0
# Section: 0.0
# Section: 0.0
# Section: 3.0
# Section: 3.0
このコードの問題点は training() 内で for 文と yield 文が繰り返し記述されていることでしょう。
見た目も「うるさい」ですし、読解性も悪くなっています。
この例ではインターバルセクションが3つだけにもかかわらずこの有様ですから、より複雑なトレーニングメニューを組むとしたら大変なことになりそうです。
このような場合の解決策は yield from 文を使うことです。
def training_composed():
yield from move(4, 5.0)
yield from stop(3)
yield from move(2, 3.0)
run(training_composed)
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 0.0
# Section: 0.0
# Section: 0.0
# Section: 3.0
# Section: 3.0
yield from move(4, 5.0)
yield from stop(3)
yield from move(2, 3.0)
run(training_composed)
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 5.0
# Section: 0.0
# Section: 0.0
# Section: 0.0
# Section: 3.0
# Section: 3.0
yield from 文は、ネストされているジェネレータに一旦処理を丸投げして、そのジェネレータが全ての値を出力し終わると元のジェネレータに制御を戻します。
つまり、ネストしているジェネレータを for 文で処理しながらその都度の値を yield 文で返す、という定型処理を自動でやってくれるんです。
for 文の記述がなくなりコードがスッキリした上に、実行速度も速い、というウレシイおまけつきです。
ネストしているジェネレータを for 文を使って処理した場合、yield from 文を使って処理した場合の処理時間を比較してみましょう。
import timeit
# ネストされるジェネレータ
def child():
for i in range(1_000_000):
yield i
# for 文を使用した従来の構文
def slow():
for i in child():
yield i
# yield from 文を使います
def fast():
yield from child()
baseline = timeit.timeit(
stmt="for _ in slow(): pass",
globals=globals(),
number=50
)
print(f"従来の構文: {baseline:.2f}s")
# 従来の構文: 5.92s
comparison = timeit.timeit(
stmt="for _ in fast(): pass",
globals=globals(),
number=50
)
print(f"yield from 文: {comparison:.2f}s")
# yield from 文: 5.08s
reduction = (baseline - comparison) / baseline
print(f"yield from 文の実行速度: {(baseline / comparison):.2%}")
# yield from 文の実行速度: 116.46%
# ネストされるジェネレータ
def child():
for i in range(1_000_000):
yield i
# for 文を使用した従来の構文
def slow():
for i in child():
yield i
# yield from 文を使います
def fast():
yield from child()
baseline = timeit.timeit(
stmt="for _ in slow(): pass",
globals=globals(),
number=50
)
print(f"従来の構文: {baseline:.2f}s")
# 従来の構文: 5.92s
comparison = timeit.timeit(
stmt="for _ in fast(): pass",
globals=globals(),
number=50
)
print(f"yield from 文: {comparison:.2f}s")
# yield from 文: 5.08s
reduction = (baseline - comparison) / baseline
print(f"yield from 文の実行速度: {(baseline / comparison):.2%}")
# yield from 文の実行速度: 116.46%
まとめ:
1: ジェネレータがネストされている場合、yield from 文を利用することでネストされているジェネレータのループ処理を記述する必要がなくなります。
2: yield from 文を利用することで処理速度の向上が期待できます。
こちらの投稿にも興味があるかもしれません...
- 【 Effective Python, 2nd Edition 】throw() メソッドを利用したジェネレータ ( generator ) 内部での状態遷移はなるだけ避けましょう。ネストが深くなってコードの読解性が落ちちゃいますよ!
- 【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻
- 【 Effective Python, 2nd Edition 】ジェネレータの反復動作中に外部から値を注入して出力結果に反映させよう! 代入式の右辺に yield 式があったり、send() メソッドを使ったり!の巻
- 【 Effective Python, 2nd Edition 】入力元のデータサイズが大きい場合は、リスト内包表記 ( list comprehension ) ではなくジェネレータ式 ( generator expression ) の利用を検討しよう!
0 comments
コメントはまだありません。
コメントを追加する(不適切と思われるコメントは削除する場合があります)