Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 479 – 제너레이터 내부의 StopIteration 처리 변경

Author:
Chris Angelico <rosuav at gmail.com>, Guido van Rossum <guido at python.org>
Status:
Final
Type:
Standards Track
Created:
15-Nov-2014
Python-Version:
3.5
Post-History:
15-Nov-2014, 19-Nov-2014, 05-Dec-2014

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 제너레이터에 대한 변경을 제안합니다. 제너레이터 내부에서 StopIteration이 발생하면 RuntimeError로 대체됩니다. (더 정확히 말하면, 이 대체는 예외가 제너레이터의 스택 프레임 밖으로 전파되려는 시점에 일어납니다.) 이 변경은 하위 호환성을 깨뜨리므로, 이 기능은 처음에는 __future__ 문을 통해 도입됩니다.

수락

이 PEP는 11월 22일에 BDFL에 의해 수락되었습니다. 첫 초안부터 수락까지의 기간이 유례없이 짧았기 때문에, 수락 이후 제기된 주요 반론들을 신중하게 검토하였으며 그 내용을 아래의 “대안 제안” 절에 반영하였습니다. 그러나 어떤 논의도 BDFL의 생각을 바꾸지 못했으며, 이제 이 PEP의 수락은 최종 확정되었습니다. (내용을 명확히 하기 위한 수정 제안은 여전히 환영합니다. IETF RFC와 달리 PEP의 본문은 수락 이후에도 고정불변이 아닙니다. 다만 핵심 설계/계획/명세는 수락 이후에 바뀌어서는 안 됩니다.)

근거

현재 제너레이터와 StopIteration의 상호작용은 다소 예상 밖이며, 잘 드러나지 않는 버그를 숨길 수 있습니다. 예상치 못한 예외는 미묘하게 달라진 동작으로 이어져서는 안 되며, 눈에 잘 띄고 디버깅하기 쉬운 트레이스백을 일으켜야 합니다. 현재는 제너레이터 함수 내부에서 실수로 발생한 StopIteration을 제너레이터를 구동하는 루프 구문이 이터레이션의 끝으로 해석합니다.

이 제안의 주된 목표는 보호되지 않은 next() 호출(아마도 스택 프레임 여러 단계 아래에 있는)이 StopIteration을 발생시켜 제너레이터가 제어하는 이터레이션을 조용히 종료시키는 상황에서 디버깅을 쉽게 하는 것입니다. (반면, 다른 예외가 발생하면 문제의 원인을 정확히 짚어 주는 트레이스백이 출력됩니다.)

이는 PEP 380의 yield from 구문과 결합될 때 특히 해로운데, 제너레이터에서 서브제너레이터를 분리해 낼 수 있다는 추상화를 깨뜨리기 때문입니다. 해당 PEP는 이 한계를 언급하면서도 “이러한 경우의 사용 사례는 드물거나 존재하지 않는다”고 적고 있습니다. 안타깝게도 의도적으로 사용하는 경우는 드물지만, 우연히 이러한 경우를 마주치기는 쉽습니다:

import contextlib

@contextlib.contextmanager
def transaction():
    print('begin')
    try:
        yield from do_it()
    except:
        print('rollback')
        raise
    else:
        print('commit')

def do_it():
    print('Refactored initial setup')
    yield # Body of with-statement is executed here
    print('Refactored finalization of successful transaction')

def gene():
    for i in range(2):
        with transaction():
            yield i
            # return
            raise StopIteration  # This is wrong
        print('Should not be reached')

for i in gene():
    print('main: i =', i)

여기서 do_it을 하위 제너레이터로 분리하면서 미묘한 버그가 생겼습니다. 감싸인 블록이 StopIteration을 발생시키면, 현재 동작에서는 이 예외가 컨텍스트 관리자에게 삼켜지고, 더 나쁜 것은 종료 처리가 조용히 건너뛰어진다는 점입니다! 비슷한 문제 동작은 asyncio 코루틴이 StopIteration을 발생시켜 조용히 종료되는 경우나, 예상과 달리 비어 있는 것으로 드러난 이터레이터에서 첫 번째 결과를 가져오기 위해 next를 사용하는 경우에도 일어납니다. 예를 들면 다음과 같습니다:

# using the same context manager as above
import pathlib

with transaction():
    print('commit file {}'.format(
        # I can never remember what the README extension is
        next(pathlib.Path('/some/dir').glob('README*'))))

두 경우 모두, 클라이언트 코드에 버그가 있으면 yield from의 리팩터링 추상화가 깨집니다.

또한 이 제안은 리스트 컴프리헨션과 제너레이터 표현식 사이의 차이를 줄여, 이 논의의 발단이 된 것과 같은 놀라운 상황 [2]을 방지합니다. 앞으로는 다음 문장들이 둘 중 하나라도 결과를 만들어 낸다면 같은 결과를 만들어 냅니다:

a = list(F(x) for x in xs if P(x))
a = [F(x) for x in xs if P(x)]

현재 상태에서는 첫 번째 형태는 (잘린) 결과를 만들어 내는 반면 두 번째 형태는 예외(즉, StopIteration)를 발생시키도록 하는 함수 F(x) 또는 술어 P(x)를 작성할 수 있습니다. 제안된 변경이 적용되면, 두 형태 모두 이 시점에서 예외를 발생시킵니다(첫 번째 경우에는 RuntimeError, 두 번째 경우에는 StopIteration이라는 차이는 있습니다).

마지막으로, 이 제안은 제너레이터를 종료하는 방법에 관한 혼란도 해소합니다. 올바른 방법은 raise StopIteration이 아니라 return입니다.

추가적인 이점으로, 위의 변경은 제너레이터 함수를 일반 함수와 훨씬 더 일관되게 만듭니다. 제너레이터로 제시된 코드 조각을 다른 것으로 바꾸고 싶다면, 보통 모든 yield를 print() 또는 list.append() 호출로 바꾸는 방식으로 꽤 간단하게 할 수 있습니다. 그러나 코드에 보호되지 않은 next() 호출이 있다면, 그것에 유의해야 합니다. 코드가 처음부터 StopIteration이 함수를 종료시키는 것에 의존하지 않고 작성되었다면, 변환은 그만큼 더 쉬워집니다.

배경 정보

__next__() (또는 send()나 throw()) 호출의 결과로 제너레이터 프레임이 (재)시작되면, 다음 세 가지 결과 중 하나가 일어날 수 있습니다:

  • yield 지점에 도달하고, yield된 값이 반환됩니다.
  • 프레임에서 반환이 일어나고, StopIteration이 발생합니다.
  • 예외가 발생하여 밖으로 전파됩니다.

뒤의 두 경우에는 프레임이 폐기됩니다(그리고 제너레이터 객체의 gi_frame 속성이 None으로 설정됩니다).

제안

제너레이터 프레임 밖으로 StopIteration이 전파되려 하면 이 예외는 RuntimeError로 대체됩니다. 그러면 (제너레이터를 호출한) next() 호출이 실패하면서 그 예외가 밖으로 전달됩니다. 그 이후로는 여느 예외와 똑같습니다. [3]

이 변경은 위에 나열한 세 번째 결과에 영향을 주며, 다른 효과는 바꾸지 않습니다. 또한 발생한 예외가 StopIteration(또는 그 서브클래스)인 경우에만 이 결과에 영향을 줍니다.

제안된 대체는 예외가 프레임 밖으로 전파되려는 시점, 즉 그 예외에 영향을 줄 수 있는 except 또는 finally 블록을 모두 빠져나온 뒤에 일어난다는 점에 유의하십시오. 프레임에서 반환하면서 발생하는 StopIteration은 영향을 받지 않습니다(요점은 StopIteration이 제너레이터가 “정상적으로” 종료되었다는 것, 즉 예외를 발생시키지 않았다는 것을 의미한다는 점입니다).

미묘한 문제는 호출자가 RuntimeError를 잡은 뒤 제너레이터 객체의 __next__() 메서드를 다시 호출하면 어떻게 되는가입니다. 답은 이 시점부터는 StopIteration을 발생시킨다는 것입니다 – 이 동작은 제너레이터가 다른 어떤 예외를 발생시켰을 때와 같습니다.

이 제안의 또 다른 논리적 귀결은 다음과 같습니다. 누군가 g.throw(StopIteration)을 사용해 제너레이터 안으로 StopIteration 예외를 던졌을 때 제너레이터가 이를 잡지 않으면(yield 주위를 try/except로 감싸서 잡을 수 있습니다), 이 예외는 RuntimeError로 변환됩니다.

전환 단계 동안에는 다음을 사용하여 모듈별로 새 기능을 활성화해야 합니다.:

from __future__ import generator_stop

이 지시문의 영향 아래에서 만들어진 모든 제너레이터 함수는 코드 객체에 REPLACE_STOPITERATION 플래그가 설정되며, 이 플래그가 설정된 제너레이터는 이 제안에 따라 동작합니다. 이 기능이 표준이 되면 플래그는 제거될 수 있으므로, 코드에서 제너레이터에 이 플래그가 있는지 검사해서는 안 됩니다.

테스트를 쉽게 하기 위해 개념 증명 패치를 만들었습니다. [4]

기존 코드에 미치는 영향

이 변경은 StopIteration이 밖으로 전파되는 것에 의존하는 기존 코드에 영향을 줍니다. groupby의 순수 Python 참조 구현 [5]에는 현재 예외가 전파된 다음 처리될 것으로 예상되는 곳에 “Exit on StopIteration”이라는 주석이 있습니다. 이런 경우는 흔하지는 않겠지만 없지도 않을 것이며, 그러한 구문은 실패하게 됩니다. 다른 예도 많습니다. 예: [6], [7].

(Alyssa Coghlan의 의견: “””제너레이터를 종료시키는 헬퍼 함수를 분리하고 싶다면, 단순히 “helper()”가 아니라 “return yield from helper()”라고 써야 합니다.”””)

(for 루프 자체에 암시된 __next__() 호출이 아니라) 표현식, 대상 또는 술어가 발생시키는 StopIteration에 의존하는 제너레이터 표현식의 예도 여기저기 있습니다.

하위 및 상위 호환 코드 작성하기

제너레이터 표현식을 빠져나가려고 StopIteration을 발생시키는 꼼수를 예외로 하면, 이전 Python 버전에서도 새로운 의미에서와 똑같이 잘 동작하는 코드를 작성하기는 쉽습니다.

제너레이터 본문에서 StopIteration이 예상되는 곳(예: 단독으로 쓰인 next() 호출이나, 경우에 따라서는 StopIteration을 발생시킬 것으로 예상되는 헬퍼 함수)을 StopIteration이 발생하면 반환하는 try/except 구문으로 감싸면 됩니다. try/except 구문은 제너레이터 함수 안에 직접 있어야 합니다. 그 자체가 제너레이터가 아닌 헬퍼 함수 안에서 이렇게 하면 동작하지 않습니다. 제너레이터 안에 raise StopIteration이 직접 나타난다면, 그냥 return으로 바꾸십시오.

깨지는 예

StopIteration을 명시적으로 발생시키는 제너레이터는 일반적으로 그냥 반환하도록 바꿀 수 있습니다. 이렇게 하면 기존의 모든 Python 버전과 호환되며, __future__의 영향도 받지 않습니다. 다음은 표준 라이브러리에서 가져온 몇 가지 예시입니다.

Lib/ipaddress.py:

if other == self:
    raise StopIteration

다음과 같이 바뀝니다:

if other == self:
    return

경우에 따라서는 yield from과 결합하여 코드를 단순화할 수 있습니다. Lib/difflib.py가 그러한 예입니다:

if context is None:
    while True:
        yield next(line_pair_iterator)

다음과 같이 바뀝니다:

if context is None:
    yield from line_pair_iterator
    return

(엄밀하게 동등한 변환을 위해서는 return이 필요하지만, 이 파일에서는 뒤따르는 코드가 없으므로 return을 생략할 수 있습니다.) 3.3 이전 버전의 Python과 호환되게 하려면 명시적인 for 루프로 작성할 수도 있습니다:

if context is None:
    for line in line_pair_iterator:
        yield line
    return

더 복잡한 이터레이션 패턴에는 명시적인 try/except 구문이 필요합니다. 예를 들어, 다음과 같은 가상의 파서는:

def parser(f):
    while True:
        data = next(f)
        while True:
            line = next(f)
            if line == "- end -": break
            data += line
        yield data

다음과 같이 다시 작성해야 합니다:

def parser(f):
    while True:
        try:
            data = next(f)
            while True:
                line = next(f)
                if line == "- end -": break
                data += line
            yield data
        except StopIteration:
            return

또는 다음과 같이 작성할 수도 있습니다:

def parser(f):
    for data in f:
        while True:
            line = next(f)
            if line == "- end -": break
            data += line
        yield data

후자의 형태는 for 루프로 파일을 이터레이션하는 것처럼 보이면서도 루프 본문에서 같은 이터레이터로부터 데이터를 더 가져오므로 이터레이션을 알아보기 어렵게 만듭니다. 하지만 이 형태는 “정상” 종료(첫 줄 대신 StopIteration이 발생하는 경우)와 “비정상” 종료(내부 루프에서 끝 표시를 찾지 못하는 경우로, 이제 RuntimeError가 발생합니다)를 명확하게 구분합니다.

StopIteration의 이러한 효과는 제너레이터 표현식을 도중에 끊어 일종의 takewhile을 만드는 데 사용되어 왔습니다:

def stop():
    raise StopIteration
print(list(x for x in range(10) if x < 5 or stop()))
# prints [0, 1, 2, 3, 4]

현재 제안에서는 이러한 형태의 비지역적 흐름 제어가 지원되지 않으므로 문장 형태로 다시 작성해야 합니다:

def gen():
    for x in range(10):
        if x >= 5: return
        yield x
print(list(gen()))
# prints [0, 1, 2, 3, 4]

이는 작은 기능 손실이지만, 이 기능은 가독성을 희생하는 대가로 얻는 경우가 많습니다. 또한 lambda가 def에 비해 제약이 있듯이, 제너레이터 표현식도 제너레이터 함수에 비해 제약이 있습니다. 많은 경우 완전한 제너레이터 함수로 바꾸는 일은 아주 쉬우며, 구조를 더 명확하게 만들 수도 있습니다.

제너레이터, 이터레이터, StopIteration에 대한 설명

이 제안은 제너레이터와 이터레이터의 관계를 바꾸지 않습니다. 제너레이터 객체는 여전히 이터레이터이며, 모든 이터레이터가 제너레이터인 것은 아닙니다. 제너레이터에는 send나 throw처럼 이터레이터에는 없는 추가 메서드가 있습니다. 이 모든 것은 바뀌지 않습니다. 제너레이터 사용자에게는 아무것도 바뀌지 않습니다. 새로운 것을 익혀야 할 수도 있는 사람은 제너레이터 함수 작성자뿐입니다. (여기에는 조건에서 발생한 StopIteration으로 이터레이션이 조기에 종료되는 것에 의존하는 제너레이터 표현식 작성자도 포함됩니다.)

이터레이터는 __next__ 메서드를 가진 객체입니다. 다른 많은 특수 메서드와 마찬가지로, 이 메서드는 값을 반환하거나, 반환할 값이 없음을 알리기 위해 특정 예외(이 경우에는 StopIteration)를 발생시킬 수 있습니다. 이 점에서 __getattr__(AttributeError를 발생시킬 수 있음), __getitem__(KeyError를 발생시킬 수 있음) 등과 비슷합니다. 이터레이터를 위한 헬퍼 함수도 같은 프로토콜을 따르도록 작성할 수 있습니다. 예를 들면 다음과 같습니다:

def helper(x, y):
    if x > y: return 1 / (x - y)
    raise StopIteration

def __next__(self):
    if self.a: return helper(self.b, self.c)
    return helper(self.d, self.e)

두 가지 신호 방식이 모두 그대로 전달됩니다. 반환된 값은 반환되고, 예외는 위로 전파됩니다. 헬퍼는 호출하는 함수의 프로토콜에 맞게 작성됩니다.

제너레이터 함수는 yield 표현식을 포함하는 함수입니다. 제너레이터 함수는 (재)시작될 때마다 값을 yield하거나 반환할 수 있습니다(“끝에 도달하는 것”도 포함합니다). 제너레이터를 위한 헬퍼 함수도 작성할 수 있지만, 이 역시 제너레이터 프로토콜을 따라야 합니다:

def helper(x, y):
    if x > y: yield 1 / (x - y)

def gen(self):
    if self.a: return (yield from helper(self.b, self.c))
    return (yield from helper(self.d, self.e))

두 경우 모두 예상치 못한 예외는 위로 전파됩니다. 제너레이터와 이터레이터의 특성상 제너레이터 안에서 예상치 못한 StopIteration이 발생하면 RuntimeError로 변환되지만, 그 밖의 모든 예외는 정상적으로 전파됩니다.

전환 계획

  • Python 3.5: __future__ 임포트 시 새 의미를 활성화합니다. __future__ 임포트를 하지 않은 제너레이터에서 StopIteration이 밖으로 전파되면 조용한 폐지 경고를 냅니다.
  • Python 3.6: 조용하지 않은 폐지 경고를 발생시킵니다.
  • Python 3.7: 모든 곳에서 새로운 의미론을 활성화합니다.

대안 제안

RuntimeError가 아닌 다른 예외 발생시키기

일반적인 RuntimeError 대신 새로운 예외 타입인 UnexpectedStopIteration을 발생시키는 것이 타당할 수도 있습니다. 이 방식에는 해당 예외를 잡도록 암묵적으로 부추긴다는 단점이 있습니다. 올바른 조치는 연쇄된 예외가 아니라 원래의 StopIteration을 잡는 것입니다.

return 시 발생시킬 특정 예외 제공하기

Alyssa (Nick) Coghlan은 제너레이터에 특정 StopIteration 인스턴스를 제공하는 방법을 제안했습니다. StopIteration의 다른 인스턴스가 발생하면 오류이지만, 바로 그 인스턴스가 발생하면 제너레이터가 정상적으로 완료된 것입니다. 이 하위 제안은 더 나은 방안이 있어 철회되었지만, 참고용으로 남겨 둡니다.

return으로 인한 StopIteration을 명확히 드러내기

특정 상황에서는 더 단순하고 하위 호환성을 완전히 유지하는 해결책으로 충분할 수 있습니다. 제너레이터가 반환할 때 StopIteration을 발생시키는 대신, 이후에 감지할 수 있는 StopIteration의 특정 서브클래스(GeneratorReturn)를 발생시키는 것입니다. 발생한 예외가 그 서브클래스가 아니라면, 그것은 return 문이 아니라 밖으로 새어 나온 예외입니다.

이 대안 제안은 Alyssa의 관찰에서 착안했습니다 [8]. asyncio 코루틴이 [9] 실수로 StopIteration을 발생시키면 현재는 아무 표시 없이 종료되므로, 개발자에게 디버깅하기 어려운 수수께끼가 될 수 있다는 것입니다. 주 제안은 이러한 실수를 명확히 구별할 수 있는 RuntimeError 예외로 바꾸지만, 그 제안이 거부된다면 이 대안 제안을 통해 asyncio가 return 문과 실수로 발생한 StopIteration 예외를 구별할 수 있게 됩니다.

위에 나열한 세 가지 결과 중 두 가지가 바뀝니다.

  • yield 지점에 도달하면, 당연히 여전히 그 값이 반환됩니다.
  • 프레임에서 반환하면 StopIteration이 아니라 GeneratorReturn이 발생합니다.
  • GeneratorReturn의 인스턴스가 발생하게 되는 경우에는, 대신 StopIteration의 인스턴스가 발생합니다. 그 밖의 예외는 평소대로 전파됩니다.

세 번째 경우, StopIteration은 원래 GeneratorReturn의 value를 가지며, __cause__에서 원래 예외를 참조합니다. 이 예외가 잡히지 않으면 예외의 연쇄가 명확하게 드러납니다.

이 대안은 제너레이터 표현식과 리스트 컴프리헨션 사이의 불일치에는 영향을 주지 않지만, 제너레이터를 인식하는 코드(contextlib 및 asyncio 모듈 등)가 위에 나열한 두 번째와 세 번째 결과를 확실하게 구별할 수 있게 해 줍니다.

그러나 GeneratorReturn과 StopIteration 사이의 이 구분에 의존하는 코드가 생기고 나면, 다른 제너레이터를 호출하면서 그 제너레이터의 StopIteration이 밖으로 전파되는 것에 의존하는 제너레이터는 두 예외 타입의 구분을 어떻게 활용하느냐에 따라 여전히 잘못될 가능성이 있습니다.

next() 내부에서 예외 변환하기

Mark Shannon은 이 문제를 제너레이터 함수의 경계가 아니라 next()에서 해결할 수 있다고 제안했습니다 [10]. next()가 StopIteration을 잡아 대신 ValueError를 발생시키게 하면, 예상치 못한 StopIteration 전파를 모두 막을 수 있습니다. 그러나 하위 호환성 문제는 현재 제안보다 훨씬 심각합니다. 이제 모든 next() 호출이 StopIteration 대신 ValueError에 대비하도록 다시 작성되어야 하기 때문입니다. 게다가 여러 Python 버전에서 안정적으로 동작하는 하나의 코드 블록을 작성할 방법도 없습니다. (ValueError를 서브클래싱하는 등의 전용 예외 타입을 사용하면 도움이 되겠지만, 그래도 모든 코드를 다시 작성해야 합니다.)

next(it, default)를 호출하면 StopIteration을 잡아서 주어진 기본값으로 대체한다는 점에 유의하십시오. 이 기능은 try/except 블록을 피하는 데 유용한 경우가 많습니다.

하위 제안: 현재 동작을 명시적으로 요청하는 데코레이터

Alyssa Coghlan은 현재 동작이 필요한 상황을 다음과 같은 데코레이터로 지원할 수 있다고 제안했습니다 [11].:

from itertools import allow_implicit_stop

@allow_implicit_stop
def my_generator():
    ...
    yield next(it)
    ...

이는 의미상 다음과 동등합니다.:

def my_generator():
    try:
        ...
        yield next(it)
        ...
    except StopIteration
        return

다만 StopIteration이 곧바로 전파되도록 허용하기만 하면 구현할 수 있으므로 더 빠릅니다.

단일 소스 Python 2/3 코드도 3.7+ 환경에서 이점을 얻게 됩니다. six나 python-future 같은 라이브러리가 3.5+에서는 새 내장 함수를 가리키고 다른 버전에서는 항등 함수로 구현되는 자체 버전의 “allow_implicit_stop”을 정의하기만 하면 되기 때문입니다.

그러나 필요한 구현의 복잡성, 지속적으로 발생하는 호환성 문제, 데코레이터 효과의 미묘함, 그리고 문제의 코드를 제대로 고치는 대신 모든 제너레이터에 데코레이터를 그냥 붙여 버리는 “임시방편” 해결책을 부추기게 된다는 점 때문에 이 하위 제안은 거부되었습니다. [12]

비판

비공식적이고 출처가 불분명한 통계에 따르면 이것이 문제가 되는 경우는 거의 또는 전혀 없습니다. [13] 현재 동작에 의존하는 코드가 실제로 존재하며(예: [3], [6], [7]), 얻는 이득은 거의 또는 전혀 없이 불필요하게 코드만 뒤흔드는 일이 될 것이라는 우려가 있습니다.

Steven D’Aprano가 comp.lang.python에서 비공식 설문 조사를 시작했습니다 [14]. 이 글을 쓰는 시점에 받은 응답은 두 건뿐입니다. 하나는 리스트 컴프리헨션을 제너레이터 표현식에 맞추어 변경하자는 의견이었고(!), 다른 하나는 이 PEP의 주요 제안에 찬성하는 의견이었습니다.

기존 모델은 예외가 특별한 의미를 갖는 다른 모든 경우에 내재된, 충분히 받아들일 만한 문제들과 비교되어 왔습니다. 예를 들어, __getitem__ 메서드 안에서 발생한 예기치 않은 KeyError는 위로 전파되도록 허용되는 대신 실패로 해석됩니다. 하지만 차이점이 있습니다. 특수 메서드는 정상을 나타내기 위해 return을, 비정상을 알리기 위해 raise를 사용합니다. 제너레이터는 데이터를 나타내기 위해 yield를, 비정상 상태를 알리기 위해 return을 사용합니다. 이 때문에 StopIteration을 명시적으로 발생시키는 것은 완전히 불필요하며, 예상치 못한 결과를 낳을 수도 있습니다. 다른 특수 메서드에도 반환 경로를 구분하는 전용 키워드가 있었다면, 그 메서드들 역시 예기치 않은 예외를 RuntimeError로 바꿀 수 있었을 것입니다. 그 메서드들이 그렇게 할 수 없다는 사실이 제너레이터가 그렇게 하는 것을 가로막아서는 안 됩니다.

모든 __next__() 메서드를 고치지 않는 이유는 무엇입니까?

일반 __next__() 메서드를 구현할 때 이터레이션의 끝을 나타내는 유일한 방법은 StopIteration을 발생시키는 것입니다. 따라서 여기에서 StopIteration을 잡아 RuntimeError로 변환하면 본래의 목적에 어긋나게 됩니다. 이는 제너레이터 함수의 특별한 지위를 다시 한번 일깨워 줍니다. 제너레이터 함수에서는 간단한 return으로 이터레이션을 종료할 수 있으므로 StopIteration을 발생시키는 것은 불필요합니다.

참고 문헌