Effective Python Item 35. 제네레이터 안에서 throw로 상태를 변화시키지 마라.

    들어가기 전

     


    요약

    • throw 메서드를 사용하면 제네레이터가 마지막으로 실행한 yield 식의 위치에서 예외를 다시 발생시킬 수 있음. 
    • throw를 사용하면 가독성이 나빠짐. 예외를 잡아내고 다시 발생시키는 데 준비 코드가 필요하며, 내포 단계가 깊어지기 때문.
    • 제네레이터에서 예외를 제공하는 더 좋은 방법은 __iter__() 메서드를 구현하는 클래스를 사용하면, 예외적인 경우에 상태를 전이시킬 수 있도록 메서드를 제공하는 것이다. 

     


    Item 35. 제네레이터 안에서 throw로 상태를 변화시키지 마라.

    제네레이터를 사용할 때, 특별한 경우에는 예외를 발생시키고 싶을 때가 있다. 그럴 때는 throw() 메서드를 이용해서 제네레이터에 예외를 주입할 수 있다. 

    # Generator를 사용할 때, throw()를 이용해서 양방향 통신을 할 수도 있다.
    # 아래와 같이 사용할 수 있음.
    
    
    class MyError(Exception):
        pass
    
    def my_generator():
        yield 1
        try:
            yield 2
        except MyError as e:
            print(f'MyError 발생. {e}')
        else:
            yield 3
        yield 4
    
    it = my_generator()
    print(next(it))
    print(next(it))
    print(it.throw(MyError("test error")))
    
    
    >>>
    1
    2
    MyError 발생. test error
    4

    위 코드는 throw()를 이용해 이터레이터에 예외를 주입하는 예시 코드다. 

    1. 2를 반환한 후에, throw() 메서드를 호출해서 제네레이터에 예외를 주입한다.
    2. 이 때 예외는 except 절에서 잡아지고, else 절은 수행되지 않는다. 따라서 제네레이터는 4라는 값을 출력한다. 

    이 코드에서 throw()를 사용해서 'try~catch'문을 사용했고,  'try ~ catch'문이 나와서 코드에 약간의 '잡음'을 만들어 낸다는 것을 알 수 있다. 즉, 제네레이터의 예외 상황을 표현하기 위해 throw()를 사용하는 것은 코드의 가독성을 해치기 때문에 권장하지 않을 것이라는 것을 미리 유추해 볼 수 있다. 

     


    throw()를 이용한 예시 코드

    class Reset(Exception):
        pass
    
    
    def timer(period):
        current = period
        while current:
            current -= 1
            try:
                yield current
            except Reset:
                current = period
    
    
    def announce(remaining):
        print(f'{remaining} 틱 남음')
    
    
    def check_for_reset():
        pass
    
    
    def run():
        it = timer(4)
        while True:
            try:
                # 리셋이 필요할 때마다, Reset을 호출한다.
                if check_for_reset():
                    current = it.throw(Reset())
                else:
                    current = next(it)
            except StopIteration:
                break
            else:
                announce(current)
    
    run()

    위 코드는 throw()를 이용해서 예외 상황을 주입하는 코드다.

    1. 타이머를 이터레이터로 만들었다. 타이머는 next()로 호출될 때 마다 남은 시간을 1씩 감소시킨다. 
    2. 타이머의 리셋이 필요한 순간을 check_for_reset() 메서드로 감시한다.
    3. 만약 리셋이 필요한 경우, throw()를 이용해 타이머에게 예외를 주입한다.
    4. 타이머는 예외를 받으면 제네레이터 코드 블록 내에서 Catch 해서 남은 시간을 초기화한다. (period = current)

    이 코드의 단점은 가독성이 나쁘다는 것이다. 이 코드에서 예외 Catch하는 곳은 두 군데인데, 이것 때문에 가독성이 나쁜 코드가 되는 것이다. 그리고 코드가 이렇게 만들어진 이유는 throw()를 이용해 제네레이터에 예외를 주입하고, 그 예외를 받으면 상태를 초기화 하도록 구성 되었기 때문이다. 

    그렇다면 어떻게 해야할까? 


    클래스를 이용한 개선

    앞서 이야기 했던 문제를 해결하기 위해 throw() 대신 컨테이너 인터페이스를 구현한 클래스를 만들고, throw()를 이용해 예외 경우를 처리하는 대신 외부에서 예외 상황에 대해서 상태 전이를 할 수 있는 코드를 제공하는 것이 낫다. 무슨 말인지 이해가 안되면 아래 코드를 살펴보자. 

    class Timer:
    
        def __init__(self, period):
            self.period = period
            self.current = period
    
        def reset(self):
            self.current = self.period
    
        def __iter__(self):
            while self.current:
                self.current -= 1
                yield self.current
    
    
    def announce(remaining):
        print(f'{remaining} 틱 남음')
    
    
    def check_for_reset():
        pass
    
    
    def run():
        timer = Timer(4)
        for current in timer:
            if check_for_reset():
                timer.reset()
            announce(current)
    
    
    run()
    1. 예외 발생 시, 상태 전이 메서드 제공 : reset()이라는 메서드를 제공한다. 클라이언트는 throw()로 예외를 주입하기 보다는 reset()을 이용해 직접 상태 전이한다. 
    2. 타이머 클래스는 이터레이터를 제공하기 위해 __iter__() 메서드를 구현하면 된다. 

    throw()로 예외 상황을 주입하는 대신, 예외 상황에서 상태를 전이할 수 있는 코드를 제공하면서 코드의 가독성이 더 좋아졌다. 결론적으로 throw()를 사용하지 않도록 하고, 이터레이터를 제공하는 클래스를 하나 구현하고 예외 상황에서 상태를 전이할 수 있는 메서드를 제공하는 것이 더 좋은 방법이 된다. 

    댓글

    Designed by JB FACTORY