betterway35 제너레이터 throw상태변화x

김승환·2021년 7월 11일

코딩의 기술

목록 보기
25/36

제너레이터 안에서 throw로 상태를 변화시키지 말라

  • yield from식과 send 메서드 외에, 제너레이터 안에서 Exception을 다시 던질 수 있는 throw 메서드가 있다.
  • throw가 작동하는 방식은 간단하다.
    - 제너레이터에 대해 throw가 호출되면 이 제너레이터는 값을 내놓은 yield로 부터 평소처럼 제너레이터 실행을 계속하는 대신
    - throw가 제공한 Exception을 다시 던진다.
# 다음 코드는 이런 동작 방식을 보여준다.
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('test error')))

#2번째 다음에 3을 부르기 전에 Myerror라는 부분이 있어야하는데 없어서 error가 발생하고 다음 순서인 3으로 넘어가지못하고 
# 제너레이터는 2까지만 실행이 되어버린것?

1
2
MyError Traceback (most recent call last)
in
13 print(next(it)) # 2를 내놓음
14 # 오류가 나는 부분. 오류를 보고 싶으면 커멘트를 해제할것
---> 15 print(it.throw(MyError('test error')))
16
17 #2번째 다음에 3을 부르기 전에 Myerror라는 부분이 있어야하는데 없어서 error가 발생하고 다음 순서인 3으로 넘어가지못하고

in my_generator()
5 def my_generator():
6 yield 1
----> 7 yield 2
8 yield 3
9

MyError: test error

  • throw를 호출해 제너레이터에 예외를 주입해도, 제너레이터는 try/except 복합문을 사용해 마지막으로 실행된 yield 문을 둘러쌈으로써 이 예외를 잡아낼 수 있다.
def my_generator():
    yield 1
    try:
        yield 2 #2를 호출하는데 다음 호출시 yield2는 사용 불가로 except를 실행하게댐
    except MyError:
        print('MyError 발생!')
    else:
        yield 3
    yield 4

it = my_generator()
print(next(it))  # 1을 내놓음
print(next(it))  # 2를 내놓음
print(it.throw(MyError('test error')))
#2까지는 무리업이 되고
#다음 순서인 myerror가 발생하고 except가 에러가 안났기 때문에 else는 넘어가고 바로 yield4 실행?

1
2
MyError 발생!
4

  • 이 기능은 제너레이터와 제너레이터를 호출하는 쪽 사이에 양방향 통신 수단을 제공한다.
  • 경우에 따라 이 양방향 통신 수단이 유용할 수 있다.
# ex) 작성하는 프로그램에 간헐적으로 재설정할 수 있는 타이머가 필요
# 다음은 throw 메서드에 의존하는 제너레이터를 통해 타이머를 구현하는 코드다.
# yield식에서 Reset 예외가 발생할 때마다 카운터가 period로 재설정 된다.
class Reset(Exception):
    pass

def timer(period):
    current = period
    while current:
        current -= 1
        try:
            yield current
        except Reset:
            current = period
  • 매 초 한 번 폴링되는 외부 입력과 이 재설정 이벤트를 연결할 수도 있다.
  • 그 후, timer 제너레이터를 구종시키는 run함수를 정의할 수 있다.
  • run 함수는 throw를 사용해 타이머를 재설정하는 예외를 주입하거나, 제너레이터 출력에 대해 announce 함수를 호출한다.
RESETS = [
    False, False, False, True, False, True, False,
    False, False, False, False, False, False, False]
#true가 나오면 period를 reset시켜서 다시 3부터 시작 
#이후 계속되다가 제너레이터가 끝나면 break
def check_for_reset():
    # 외부 이벤트를 폴링한다
    return RESETS.pop(0) #하나씩 끄집어내고 원래 변수에서는 삭제

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를 호출할지 결정하는데 , 이로인해 잡음이많다.

  • 이 기능을 구현하는 더 단순한 방법은 이터러블 컨테이너 객체를 사용해 상태가 있는 클로저를 정의하는 것이다.

  • 이러한 클래스를 사용해 timet 제너레이터를 재정의한 코드는 다음과 같다.

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를 사용해 훨씬 단순하게 이터레이션을 수행할 수 있고, 내포 수준이 줄어들어 코드가 훨씬 읽기 쉽다.

RESETS = [
    False, False, False, True, False, True, False,
    False, False, False, False, False, False, False]
def run():
    timer = Timer(4)
    for current in timer:
        if check_for_reset():
            timer.reset()
        announce(current)

run()

3 틱 남음
2 틱 남음
1 틱 남음
0 틱 남음
3 틱 남음
2 틱 남음
3 틱 남음
2 틱 남음
1 틱 남음
0 틱 남음

  • 출력은 throw를 사용하던 예전 버전과 똑같지만, 훨씬 더 이해하기 쉽게 구현됐다.
  • 제너레이터와 예외를 섞어서 만들어야 하는 작업이 있다면, 비동기 기능을 사용하면 더 좋게 구현할 수 있는 경우도 많다.
  • 따라서 예외적인 경우를 처리해야 한다면 throw를 전혀 사용하지 말고 이터러블 클래스를 사용할것
profile
인공지능 파이팅!

0개의 댓글