cover_effective_python

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

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

Published 2020年7月4日23:31 by T.Tsuyoshi

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__() メソッドを実装したコンテナクラスを記述することです。

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

0 comments

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

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