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 380yield 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)]

현재 상태에서는 함수 F(x)또는 술어 P(x)를 작성하여 첫 번째 형식은 (잘린) 결과를 생성하게 하고 두 번째 형식은 예외(즉, StopIteration)를 발생시키게 할 수 있습니다. 제안된 변경에 따르면 이 지점에서 두 형식 모두 예외를 발생시킵니다. (첫 번째 경우에는 RuntimeError이고 두 번째 경우에는 StopIteration이라는 차이는 있습니다.)

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

추가적인 이점으로, 위의 변경 사항은 제너레이터 함수를 일반 함수에 훨씬 더 가깝게 만듭니다. 제너레이터로 제시된 코드의 일부를 가져와 다른 것으로 바꾸려는 경우, 일반적으로 모든 yieldprint()또는 list.append()를 호출하는 것으로 대체하면 매우 간단하게 처리할 수 있습니다. 그러나 코드에 단독 next()호출이 있다면 이를 인지하고 있어야 합니다. 코드가 원래 StopIteration이 함수를 종료하는 데 의존하지 않고 작성되었다면 변환은 훨씬 더 쉬워집니다.

배경 정보

__next__() 호출(또는 send() 호출 또는 throw() 호출)의 결과로 제너레이터 프레임이 시작되거나 다시 시작되면 세 가지 결과 중 하나가 발생할 수 있습니다.

  • 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 [5]의 순수 Python 참조 구현에는 현재 예외가 전파된 후 처리될 것으로 예상되는 부분에 “Exit on StopIteration”이라는 주석이 있습니다. 이는 흔하지는 않지만 알려지지 않은 현상은 아니며, 이러한 구성은 실패합니다. 그 밖에도 예는 많습니다. 예를 들면 [6], [7]이 있습니다.

(Alyssa Coghlan의 설명: “””제너레이터를 종료하는 도우미 함수를 분리하려면 단순히 “helper()”라고 하는 대신 “return yield from helper()”라고 해야 합니다.”””)

또한 __next__()호출에 의해서가 아니라 for루프 자체에서 암시되는 호출에 의해서 표현식, 대상 또는 조건자가 발생시킨 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을 생략할 수 있습니다.) Python 3.3 이전 버전과의 호환성을 위해 명시적인 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]

이는 기능 면에서 작은 손실이지만, 가독성을 희생하는 대가로 얻는 경우가 많은 기능이며, lambdadef에 비해 제약을 가지는 것과 마찬가지로 제너레이터 표현식도 제너레이터 함수에 비해 제약을 가집니다. 많은 경우 전체 제너레이터 함수로의 변환은 매우 간단하며, 구조적 명확성을 향상할 수도 있습니다.

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

이 제안은 제너레이터와 이터레이터 간의 관계를 변경하지 않습니다. 제너레이터 객체는 여전히 이터레이터이며, 모든 이터레이터가 제너레이터인 것은 아닙니다. 제너레이터에는 이터레이터에는 없는 추가 메서드가 있으며, sendthrow가 그 예입니다. 이 모든 사항은 변경되지 않습니다. 제너레이터 사용자에게는 아무것도 변경되지 않으며 – 새로운 내용을 배워야 할 수 있는 사람은 제너레이터 함수 작성자뿐입니다. (여기에는 조건에서 발생한 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을 잡는 것입니다.

반환 시 발생시킬 특정 예외 제공

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

반환으로 발생하는 StopIteration을 명확하게 만들기

특정 상황에서는 더 단순하고 완전히 하위 호환 가능한 해결책으로 충분할 수 있습니다. 제너레이터가 반환할 때 StopIteration을 발생시키는 대신, 감지할 수 있는 특정 StopIteration의 서브클래스인 GeneratorReturn을 발생시키는 방법입니다. 해당 예외가 그 서브클래스가 아니라면 반환 문이 아니라 외부로 전파되는 예외입니다.

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

위에 나열한 세 가지 결과 중 두 가지가 변경됩니다.

  • yield 지점에 도달하면 값은 물론 여전히 반환됩니다.
  • 프레임에서 반환하면 StopIteration대신 GeneratorReturn이 발생합니다.
  • GeneratorReturn의 인스턴스가 발생할 상황에서는 대신 StopIteration의 인스턴스가 발생합니다. 그 밖의 예외는 정상적으로 상위로 전파됩니다.

세 번째 경우 StopIteration은 원래 GeneratorReturnvalue를 가지며, __cause__에서 원래 예외를 참조합니다. 잡히지 않으면 예외의 연쇄가 명확하게 표시됩니다.

이 대안은 제너레이터 표현식과 리스트 컴프리헨션 간의 불일치에 영향을 주지 않지만, 제너레이터를 인식하는 코드(예: contextlibasyncio 모듈)가 위에 나열한 두 번째 결과와 세 번째 결과를 안정적으로 구별할 수 있게 합니다.

그러나 GeneratorReturnStopIteration의 이러한 구별에 의존하는 코드가 존재하게 되면, 다른 제너레이터를 호출하고 그 제너레이터의 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이 직접 전파되도록 허용하기만 하면 구현할 수 있으므로 더 빠릅니다.

3.7 이상 환경에서는 단일 소스 Python 2/3 코드도 이점을 얻을 수 있습니다. 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을 발생시키는 것이 불필요합니다.

참고 자료