cover_effective_python

【 Effective Python, 2nd Edition 】yield from ステートメントでネストしたジェネレータ ( nested generators, composed generators ) を効率よく処理しよう! 投稿一覧へ戻る

Tags: Python , generator , yield , Effective , from

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



実際のトレーニングプログラムを作成するときには、これら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



このコードの問題点は 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 文は、ネストされているジェネレータに一旦処理を丸投げして、そのジェネレータが全ての値を出力し終わると元のジェネレータに制御を戻します。
つまり、ネストしているジェネレータを 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%



まとめ:

1: ジェネレータがネストされている場合、yield from 文を利用することでネストされているジェネレータのループ処理を記述する必要がなくなります。
2: yield from 文を利用することで処理速度の向上が期待できます。

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

0 comments

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

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