제너레이터는 안에서 Exception
을 다시 던질 수 있는 throw
메서드가 있다. 어떤 제너레이터에 대해 throw
가 호출되면 이 제너레이터는 값을 내놓은 yield
로 부터 평소처럼 제너레이터 실행을 계속하는 대신, throw
가 제공한 Exception
을 다시 던진다.
class MyError(Exception):
pass
def my_generator():
yield 1
yield 2
yield 3
it = my_generator()
print(next(it))
print(next(it))
print(it.throw(MyError('test error')))
>>>
1
2
Traceback ...
MyError : test error
throw
를 호출해 제너레이터에 예외를 주입해도, 제너레이터는 try/except
복합문을 사용해 마지막으로 실행된 yield
문을 둘러쌈으로써 이 예외를 잡아낼 수 있다.
def my_generator():
yield 1
try:
yield 2
except MyError:
print('MyError 발생!')
else:
yield 3
yield 4
it = my_generator()
print(next(it))
print(next(it))
print(it.throw(MyError('test error')))
>>>
1
2
MyError 발생!
4
이 기능은 제너레이터와 제너레이터를 호출하는 쪽 사이에 양방향 통신 수단을 제공한다. 예를 들어 작성하는 프로그램에 간헐적으로 재설정할 수 있는 타이머가 필요하다고 할때
class Reset(Exception):
pass
def timer(period):
current = period
while current:
current -= 1
try:
yield current
except Reset:
current = period
yield
식에서 Reset
예외가 발생할 때마다 카운터가 period
로 재설정된다.
매 초 한 번 폴링(polling)되는 외부 입력과 이 재설정 이벤트를 연결할 수도 있다. 그 후 timer
제너레이터를 구동시키는 run
함수를 정의할 수 있다.
def check_for_reset():
# 외부 이벤트를 폴링 (재설정 이벤트)
...
def announce(remaining):
print(f'{remaining} 틱 남음')
def run():
it = timer(4)
while True:
try:
if check_for_reset():
current = it.throw(Reset())
else:
current = next(it)
except StopIteration:
break
else:
announce(current)
run()
>>>
3 틱 남음
2 틱 남음
1 틱 남음
3 틱 남음
2 틱 남음
3 틱 남음
2 틱 남음
1 틱 남음
0 틱 남음
위 코드는 잘 작동하지만, 필요 이상으로 읽기 어렵다. 각 내포 단계마다 StopIteration
예외를 잡아내거나 throw
를 할지, next
나 announce
를 호출할지 결정하는데, 이로 인해 코드에 잡음이 많다.
이 기능을 구현하는 더 단순한 접근 방법은 이터러블 컨테이너 객체를 사용해 상태가 있는 클로저를 정의하는 것이다.
class Timer:
def __init__(self, period):
self.current = period
self.period = period
def reset(self):
self.current = self.period
def __iter__(self):
while self.current:
self.current -= 1
yield self.current
이제 run
메서드에서는 for
를 사용해 훨씬 단순하게 이터레이션을 수행할 수 있고, 내포 수준이 줄어들어 코드가 훨씬 읽기 쉽다.
def run():
timer = Timer(4)
for current in timer:
it check_for_reset():
timer.reset()
announce(current)
run()
>>>
3 틱 남음
2 틱 남음
1 틱 남음
3 틱 남음
2 틱 남음
3 틱 남음
2 틱 남음
1 틱 남음
0 틱 남음
출력은 throw
를 사용하던 코드와 똑같지만, 훨씬 더 이해하기 쉽게 구현되었다. 제너레이터와 예외를 섞어서 만들어야 하는 작업이 있다면, 비동기 기능을 사용하면 더 좋게 구현할 수 있는 경우도 많다.
기억해야 할 내용
throw
메서드를 사용하면 제너레이터가 마지막으로 실행한yield
식의 위치에서 예외를 다시 발생시킬 수 있다.throw
를 사용하면 가독성이 나빠진다. 예외를 잡아내고 다시 발생시키는 데 준비 코드가 필요하며 내포 단계가 깊어지기 때문이다.- 제너레이터에서 예외적인 동작을 제공하는 더 나은 방법은
__iter__
메서드를 구현하는 클래스를 사용하면서 예외적인 경우 상태를 전이시키는 것이다.