【 Effective Python, 2nd Edition 】yield from ステートメントでネストしたジェネレータ ( nested generators, composed generators ) を効率よく処理しよう! 投稿一覧へ戻る
Published 2020年7月1日12:32 by mootaro23
SUPPORT UKRAINE
- Your indifference to the act of cruelty can thrive rogue nations like Russia -
ジェネレータを利用することでメモリ消費を抑えることができたり、イテラブル可能な独自クラスを簡単に実装できたり、と多くの利点を享受することができます。
今回は、そんなジェネレータがネストしている場合の効率的な実行方法についてです。
さて、インターバルトレーニング用のプログラムを作りたいと思っています。
最初は速いペースで、ちょっと休んで、次はゆっくりとしたペースで、という指示を出せるようにしたいんです。
そこで、次のような2つのジェネレータを用意しました。
実際のトレーニングプログラムを作成するときには、これら2つのジェネレータを組み合わせて一連の流れを組み立てます。
各インターバルセクション毎にそれぞれのジェネレータを実行し、結果的に1つのトレーニングプログラムを作成します。
このコードの問題点は training() 内で for 文と yield 文が繰り返し記述されていることでしょう。
見た目も「うるさい」ですし、読解性も悪くなっています。
この例ではインターバルセクションが3つだけにもかかわらずこの有様ですから、より複雑なトレーニングメニューを組むとしたら大変なことになりそうです。
このような場合の解決策は yield from 文を使うことです。
yield from 文は、ネストされているジェネレータに一旦処理を丸投げして、そのジェネレータが全ての値を出力し終わると元のジェネレータに制御を戻します。
つまり、ネストしているジェネレータを for 文で処理しながらその都度の値を yield 文で返す、という定型処理を自動でやってくれるんです。
for 文の記述がなくなりコードがスッキリした上に、実行速度も速い、というウレシイおまけつきです。
ネストしているジェネレータを for 文を使って処理した場合、yield from 文を使って処理した場合の処理時間を比較してみましょう。
まとめ:
今回は、そんなジェネレータがネストしている場合の効率的な実行方法についてです。
さて、インターバルトレーニング用のプログラムを作りたいと思っています。
最初は速いペースで、ちょっと休んで、次はゆっくりとしたペースで、という指示を出せるようにしたいんです。
そこで、次のような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 文を利用することで処理速度の向上が期待できます。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -