検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
effective_python

【 Effective Python, 2nd Edition 】throw() メソッドを利用したジェネレータ ( generator ) 内部での状態遷移はなるだけ避けましょう。ネストが深くなってコードの読解性が落ちちゃいますよ! 投稿一覧へ戻る

Published 2020年7月4日23:31 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

yield from 文や send() メソッドに加えて、ジェネレータを使う際にあまり利用されていない機能として throw() メソッドがあります。


ジェネレータが throw() メソッドによって呼び出されると、再開した時点の、すなわち、前回終了した時点の yield 式自体が渡された例外を即座に投げます。


そして、ジェネレータ内でその例外をキャッチしない場合、呼び出し元にその例外を投げてジェネレータはその時点で動作を終了します。


class MyError(Exception):
pass


def my_generator():
yield 1
yield 2
yield 3


it = my_generator()

print(next(it))
# 1

print(next(it))
# 2

print(it.throw(MyError('エラーだよ')))
# Traceback (most recent call last):
# __main__.MyError: エラーだよ



yield 2 で停止していたジェネレータを throw(MyError()) メソッドで再開しましたが、
ジェネレータ内ではこの例外をキャッチしていないので、yield 3 を実行することなく例外を投げて終了しています。


しかし、例外を発生させる yield 式を try / except ブロックで囲んでジェネレータ内部でその例外をキャッチした場合は、
例外を発生させてから次の yield 式まで実行して値を返すか、返す値が存在していなければ StopIteration 例外を投げます



def my_generator_try():
yield 1

try:
yield 2
except MyError:
print('MyError 例外が発生した模様です!')
else:
yield 3

yield 4


it = my_generator_try()

print(next(it))
# 1

print(next(it))
# 2

print(it.throw(MyError('エラーだよ')))
# MyError 例外が発生した模様です!
# 4



ねっ、yield 2 で停止していたジェネレータを throw(MyError()) メソッドで再開、例外がキャッチされてエラーメッセージが表示され、次の yield 4 が実行されて停止しています。
もし yield 4 ステートメントがなければ StopIteration 例外が発生しますので試してください。


ここまで見てきたように、send() メソッドと同様、throw() メソッドを利用してもジェネレータと双方向コミュニケーションを築くことができる、ということなんです。


例えば、渡された数値からのカウントダウン値を返すジェネレータ関数があり、外部の他の関数の結果によってそのカウントダウンを続行したりリセットしたりするようなプログラムを throw() メソッドを利用して記述してみましょう。


class Reset(Exception):
pass


def countdown(count):
"""
カウントダウン用ジェネレータです。
throw(Reset()) で呼び出された場合、current = count でカウントダウン値を初期値に戻し、
yield current で値を返して停止します。
"""
current = count
while current:
current -= 1

try:
yield current
except Reset:
current = count


def outside_program():
"""
これを外部のプログラムの代わりとします。
このプログラムの実行結果が、
True の場合はカウントダウンをリセットし、False の場合はカウントダウンを続行します。
"""
result = [False, False, False, True, False, True, False, False, False]
for r in result:
yield r


def run_with_throw():
"""
外部プログラム (outside_program() で代用) の実行結果によって、
カウントダウンを続行するか、リセットするかを決定します。
"""
it = countdown(4)
result_it = outside_program()
while True:
try:
if next(result_it):
current = it.throw(Reset())
else:
current = next(it)
except StopIteration:
break
else:
print(f"残り: {current}")


run_with_throw()
# 残り: 3
# 残り: 2
# 残り: 1
# 残り: 3
# 残り: 2
# 残り: 3
# 残り: 2
# 残り: 1
# 残り: 0



期待通りに動作していますね。
でも、run_with_throw() 関数のネストが深めでちょっと読み辛くはありませんか?


ジェネレータを next() で呼び出すのか、それとも throw() で呼び出すのか判断して、
カウントダウンが終了したかどうか StopIteration 例外をキャッチできるようにして、
StopIteration 以外のときはカウントダウン文字列を出力して。


これと同じ機能を実現するもっとスマートな方法はイテラブル可能なコンテナクラスを定義してしまうことです。


class Timer:
def __init__(self, count):
self.count = count
self.current = count

def reset(self):
self.current = self.count - 1
return self.current

def __iter__(self):
while self.current:
self.current -= 1
yield self.current


def run_with_container():
timer = Timer(4)
result_it = outside_program()
for current in timer:
if next(result_it):
current = timer.reset()
print(f"残り: {current}")


run_with_container()
# 残り: 3
# 残り: 2
# 残り: 1
# 残り: 3
# 残り: 2
# 残り: 3
# 残り: 2
# 残り: 1
# 残り: 0



throw() メソッドを利用したバージョンとまったく同じ結果が得られました。
run_with_container() 関数の読み易さはいかがでしょうか? ちょっとスッキリしていて分かりやすくはないですか?


1つだけ注意していただきたいのは、リセットがかかった場合の処理です。


throw() メソッドを利用したバージョンでは、カウント数をリセットし、while ループの先頭に戻ってカウント数を -1 し、yield 文でその値を出力して停止します。


ですから、コンテナクラスバージョンでも同様の処理を再現するために、元のカウントダウン数 (self.count) を -1 して self.current にセットし、その値を返すようにしています。


まとめ:

1: throw() メソッドを利用することで、ジェネレータ内で例外を発生させることが可能になります。この例外は前回のイテレート動作が終了した時点の yield 式で発生します。

2: throw() メソッドを利用する場合は例外処理や条件分岐を記述せざるを得ず、ネストが深くなり読解性が落ちやすくなります。

3: throw() メソッドを利用した場合と同様の機能を実現するより良い方法は、例外発生時の状態遷移を模倣する関数と __iter__() メソッドを実装したコンテナクラスを記述することです。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻
【 Effective Python, 2nd Edition 】ジェネレータの反復動作中に外部から値を注入して出力結果に反映させよう! 代入式の右辺に yield 式があったり、send() メソッドを使ったり!の巻
【 Effective Python, 2nd Edition 】組み込みタイプ ( built-in types ) を利用していてネストが深くなってきたらクラス ( class ) を作成する頃合いです、の巻
【 Effective Python, 2nd Edition 】yield from ステートメントでネストしたジェネレータ ( nested generators, composed generators ) を効率よく処理しよう!
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
【 Effective Python, 2nd Edition 】入力元のデータサイズが大きい場合は、リスト内包表記 ( list comprehension ) ではなくジェネレータ式 ( generator expression ) の利用を検討しよう!
【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです!