[Final] PEP 380 - Syntax for Delegating to a Subgenerator
원문 링크: PEP 380 - Syntax for Delegating to a Subgenerator
상태: Final 유형: Standards Track 작성일: 13-Feb-2009
PEP 380 – 서브제너레이터 위임 문법 (Syntax for Delegating to a Subgenerator)
- 작성자: Gregory Ewing
- 상태: 최종 (Final)
- 유형: 표준 트랙 (Standards Track)
- 생성일: 2009년 2월 13일
- Python 버전: 3.3
- 해결: Python-Dev 메시지
개요 (Abstract)
이 PEP는 제너레이터가 자신의 작업 일부를 다른 제너레이터에게 위임(delegate)할 수 있는 새로운 문법을 제안합니다. 이 문법을 통해 yield
를 포함하는 코드 섹션을 별도의 제너레이터로 분리(factor out)하여 재사용할 수 있게 됩니다. 또한, 서브제너레이터는 값을 가지고 return
할 수 있으며, 이 반환 값은 위임하는 제너레이터(delegating generator)에게 제공됩니다.
새로운 문법은 한 제너레이터가 다른 제너레이터가 생성한 값을 재-yield할 때 최적화할 수 있는 기회도 제공합니다.
PEP 승인 (PEP Acceptance)
Guido van Rossum은 2011년 6월 26일에 이 PEP를 공식적으로 승인했습니다.
동기 (Motivation)
Python의 제너레이터는 코루틴(coroutine)의 한 형태이지만, 즉각적인 호출자(immediate caller)에게만 yield
할 수 있다는 한계가 있었습니다. 이는 yield
를 포함하는 코드 조각을 다른 코드처럼 별도의 함수로 분리할 수 없다는 것을 의미합니다. 이러한 분리를 시도하면 호출된 함수 자체가 제너레이터가 되고, 이 두 번째 제너레이터를 명시적으로 순회(iterate)하며 생성되는 모든 값을 재-yield해야 합니다.
단순히 값을 yield하는 것만 고려한다면, 다음과 같은 루프를 사용하여 큰 어려움 없이 수행할 수 있습니다.
for v in g:
yield v
그러나 서브제너레이터가 send()
, throw()
, close()
호출의 경우 호출자와 제대로 상호작용하려면 상황이 훨씬 복잡해집니다. 아래에서 보겠지만, 필요한 코드는 매우 복잡하며 모든 엣지 케이스를 올바르게 처리하기 어렵습니다.
이 문제를 해결하기 위해 새로운 문법이 제안됩니다. 가장 간단한 사용 사례에서는 위의 for
루프와 동일하지만, 제너레이터의 모든 동작 범위를 처리하고 제너레이터 코드를 간단하고 직관적인 방식으로 리팩토링할 수 있도록 합니다.
제안 (Proposal)
제너레이터의 본문에서 다음과 같은 새로운 표현식 문법이 허용됩니다.
yield from <expr>
여기서 <expr>
은 이터러블(iterable)로 평가되는 표현식이며, 이터러블에서 이터레이터(iterator)가 추출됩니다. 이 이터레이터는 소진될 때까지 실행되며, 이 시간 동안 yield from
표현식을 포함하는 제너레이터(즉, “위임하는 제너레이터”)의 호출자에게 직접 값을 yield하고 받습니다.
또한, 이터레이터가 다른 제너레이터인 경우, 서브제너레이터는 값을 가지고 return
문을 실행할 수 있으며, 이 값은 yield from
표현식의 값이 됩니다.
yield from
표현식의 전체 의미론은 제너레이터 프로토콜(generator protocol) 관점에서 다음과 같이 설명할 수 있습니다.
- 이터레이터가 yield하는 모든 값은 호출자에게 직접 전달됩니다.
send()
를 사용하여 위임하는 제너레이터로 보내진 모든 값은 이터레이터에게 직접 전달됩니다.- 보내진 값이
None
이면 이터레이터의__next__()
메서드가 호출됩니다. - 보내진 값이
None
이 아니면 이터레이터의send()
메서드가 호출됩니다. - 호출이
StopIteration
을 발생시키면 위임하는 제너레이터가 재개됩니다. - 다른 예외는 위임하는 제너레이터로 전파됩니다.
- 보내진 값이
GeneratorExit
외의 예외가 위임하는 제너레이터로throw
되면, 이터레이터의throw()
메서드로 전달됩니다.- 호출이
StopIteration
을 발생시키면 위임하는 제너레이터가 재개됩니다. - 다른 예외는 위임하는 제너레이터로 전파됩니다.
- 호출이
GeneratorExit
예외가 위임하는 제너레이터로throw
되거나, 위임하는 제너레이터의close()
메서드가 호출되면, 이터레이터에close()
메서드가 있는 경우 이터레이터의close()
메서드가 호출됩니다.- 이 호출이 예외를 발생시키면, 위임하는 제너레이터로 전파됩니다.
- 그렇지 않으면
GeneratorExit
가 위임하는 제너레이터에서 발생합니다.
yield from
표현식의 값은 이터레이터가 종료될 때 발생하는StopIteration
예외의 첫 번째 인자입니다.- 제너레이터에서
return expr
은 제너레이터 종료 시StopIteration(expr)
이 발생하도록 합니다.
StopIteration 개선 사항 (Enhancements to StopIteration)
편의를 위해 StopIteration
예외에는 첫 번째 인자를 담는 value
속성이 부여됩니다. 인자가 없으면 None
이 됩니다.
형식 의미론 (Formal Semantics)
이 섹션에서는 Python 3 문법이 사용됩니다.
RESULT = yield from EXPR
문은 의미론적으로 다음과 동등합니다.
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
제너레이터에서 return value
문은 의미론적으로 다음과 동등합니다.
raise StopIteration(value)
다만, 현재와 마찬가지로 이 예외는 반환하는 제너레이터 내의 except
절에 의해 포착될 수 없습니다.
StopIteration
예외는 다음과 같이 정의된 것처럼 동작합니다.
class StopIteration(Exception):
def __init__(self, *args):
if len(args) > 0:
self.value = args[0]
else:
self.value = None
Exception.__init__(self, *args)
근거 (Rationale)
리팩토링 원칙 (The Refactoring Principle)
위에 제시된 의미론의 대부분은 제너레이터 코드를 리팩토링할 수 있도록 하려는 염원에서 비롯됩니다. 하나 이상의 yield
표현식을 포함하는 코드 섹션을 가져와 별도의 함수로 이동하고(주변 스코프의 변수 참조를 처리하는 일반적인 기술 등을 사용하여), yield from
표현식을 사용하여 새 함수를 호출할 수 있어야 합니다.
결과적으로 생성된 복합 제너레이터의 동작은 __next__()
, send()
, throw()
, close()
호출을 포함한 모든 상황에서 합리적으로 가능한 한 원래의 리팩토링되지 않은 제너레이터와 동일해야 합니다.
제너레이터 이외의 서브이터레이터(subiterator)의 경우 의미론은 제너레이터 케이스의 합리적인 일반화로 선택되었습니다.
제안된 의미론은 리팩토링과 관련하여 다음과 같은 제약이 있습니다.
GeneratorExit
를 포착하고 나중에 다시 발생시키지 않는 코드 블록은 정확히 동일한 동작을 유지하면서 분리될 수 없습니다.StopIteration
예외가 위임하는 제너레이터로throw
되는 경우, 분리된 코드가 분리되지 않은 코드와 다르게 동작할 수 있습니다.
이러한 사용 사례는 거의 없거나 존재하지 않으므로, 이를 지원하는 데 필요한 추가적인 복잡성을 감수할 가치가 없다고 판단되었습니다.
종료 처리 (Finalization)
yield from
에서 일시 중단된 위임하는 제너레이터를 close()
메서드를 호출하여 명시적으로 종료할 때 서브이터레이터도 종료해야 하는지에 대한 논쟁이 있었습니다. 그렇게 하는 것에 반대하는 주장은 다른 곳에 서브이터레이터에 대한 참조가 존재할 경우 서브이터레이터가 조기에 종료될 수 있다는 것이었습니다.
비-참조 카운팅(non-refcounting) Python 구현을 고려한 결과, 이 명시적 종료를 수행해야 한다는 결정이 내려졌습니다. 이는 분리된 제너레이터를 명시적으로 닫는 것이 모든 Python 구현에서 분리되지 않은 제너레이터를 닫는 것과 동일한 효과를 갖도록 하기 위함입니다.
대부분의 사용 사례에서 서브이터레이터는 공유되지 않을 것이라는 가정이 있습니다. 공유된 서브이터레이터의 드문 경우는 throw()
및 close()
호출을 차단하는 래퍼(wrapper)를 사용하거나, yield from
이 아닌 다른 수단을 사용하여 서브이터레이터를 호출함으로써 해결할 수 있습니다.
스레드로서의 제너레이터 (Generators as Threads)
제너레이터가 값을 반환할 수 있도록 하는 동기는 경량 스레드(lightweight threads)를 구현하기 위해 제너레이터를 사용하는 것과 관련이 있습니다. 그런 방식으로 제너레이터를 사용할 때, 경량 스레드에 의해 수행되는 계산을 여러 함수에 분산시키기를 원하는 것은 합리적입니다. 서브제너레이터를 일반 함수처럼 호출하여 매개변수를 전달하고 반환 값을 받을 수 있기를 바랍니다.
제안된 문법을 사용하면, 일반 함수 f
에 대한 y = f(x)
와 같은 문을 위임 호출 y = yield from g(x)
로 변환할 수 있습니다. 여기서 g
는 제너레이터입니다. g
를 yield
문을 사용하여 일시 중단될 수 있는 일반 함수로 생각함으로써 결과 코드의 동작을 추론할 수 있습니다.
이러한 방식으로 제너레이터를 스레드로 사용할 때, 일반적으로 yield
를 통해 전달되거나 나가는 값에는 관심이 없습니다. 그러나 스레드가 항목의 생산자 또는 소비자로 간주되는 사용 사례도 있습니다. yield from
표현식을 사용하면 스레드의 로직을 원하는 만큼 많은 함수에 분산시킬 수 있으며, 항목의 생산 또는 소비는 어떤 하위 함수에서든 발생하고, 해당 항목은 궁극적인 원본 또는 대상으로 자동으로 라우팅됩니다.
throw()
및 close()
와 관련하여, 외부에서 스레드로 예외가 throw
되면 가장 안쪽의 제너레이터(스레드가 일시 중단된 곳)에서 먼저 발생해야 하고 거기서부터 바깥쪽으로 전파되어야 하며, close()
를 호출하여 외부에서 스레드가 종료되면 활성 제너레이터의 체인이 가장 안쪽에서부터 바깥쪽으로 종료되어야 한다는 것은 합리적인 기대입니다.
문법 (Syntax)
제안된 특정 문법은 의미를 암시하는 동시에 새로운 키워드를 도입하지 않고 일반 yield
와는 다른 점이 분명하게 드러나도록 선택되었습니다.
최적화 (Optimisations)
특수화된 문법을 사용하면 제너레이터 체인이 길어질 때 최적화 가능성이 열립니다. 이러한 체인은 예를 들어, 트리 구조를 재귀적으로 순회할 때 발생할 수 있습니다. __next__()
호출 및 yield된 값을 체인 아래로, 위로 전달하는 오버헤드는 최악의 경우 O(n) 연산을 O(n**2)로 만들 수 있습니다.
가능한 전략은 제너레이터 객체에 위임될 제너레이터를 담는 슬롯을 추가하는 것입니다. 제너레이터에서 __next__()
또는 send()
호출이 이루어지면, 이 슬롯을 먼저 확인하고 비어 있지 않으면 참조하는 제너레이터가 대신 재개됩니다. StopIteration
이 발생하면 슬롯이 지워지고 주 제너레이터가 재개됩니다.
이는 위임 오버헤드를 Python 코드 실행 없이 C 함수 호출 체인으로 줄일 수 있습니다. 가능한 개선 사항은 루프에서 전체 제너레이터 체인을 순회하고 끝에 있는 제너레이터를 직접 재개하는 것이지만, 이때 StopIteration
처리가 더 복잡해집니다.
StopIteration을 이용한 값 반환 (Use of StopIteration to return values)
제너레이터의 반환 값을 되돌려줄 수 있는 다양한 방법이 있습니다. 일부 대안으로는 제너레이터-이터레이터 객체의 속성으로 저장하거나, 서브제너레이터에 대한 close()
호출의 값으로 반환하는 것이 포함됩니다. 그러나 제안된 메커니즘은 몇 가지 이유로 매력적입니다.
StopIteration
예외의 일반화를 사용하면 다른 종류의 이터레이터가 추가 속성이나close()
메서드를 추가할 필요 없이 프로토콜에 참여하기 쉽습니다.- 서브제너레이터에서 반환 값이 사용 가능해지는 시점이 예외가 발생하는 시점과 동일하기 때문에 구현이 간단해집니다. 나중에 지연하면 반환 값을 어딘가에 저장해야 합니다.
거부된 아이디어 (Rejected Ideas)
일부 아이디어는 논의되었으나 거부되었습니다.
- 제안:
__next__()
에 대한 초기 호출을 방지하거나 지정된 값으로send()
호출로 대체하는 방법이 있어야 하며, 이는 초기__next__()
가 자동으로 수행되도록 래핑된 제너레이터 사용을 지원하려는 의도였습니다.- 해결: 이 제안의 범위를 벗어납니다. 이러한 제너레이터는
yield from
과 함께 사용되어서는 안 됩니다.
- 해결: 이 제안의 범위를 벗어납니다. 이러한 제너레이터는
- 제안: 서브이터레이터를 닫는 것이 값을 가진
StopIteration
을 발생시키면, 위임하는 제너레이터에 대한close()
호출에서 해당 값을 반환합니다.- 이 기능의 동기는 제너레이터로 보내지는 값 스트림의 끝을 제너레이터를 닫음으로써 알릴 수 있도록 하기 위함입니다. 제너레이터는
GeneratorExit
를 포착하고, 계산을 완료하며, 결과를 반환하고, 이 결과는close()
호출의 반환 값이 됩니다. - 해결:
close()
및GeneratorExit
의 이러한 사용은 bail-out 및 정리 메커니즘으로서의 현재 역할과 호환되지 않습니다. 이는 위임하는 제너레이터를 닫을 때, 서브제너레이터가 닫힌 후GeneratorExit
를 다시 발생시키는 대신 위임하는 제너레이터가 재개되어야 함을 요구합니다. 그러나 이는 허용될 수 없습니다. 왜냐하면close()
가 정리 목적으로 호출되는 경우 위임하는 제너레이터가 제대로 종료되도록 보장하지 못할 것이기 때문입니다. - 소비자에게 값의 끝을 알리는 것은 센티넬(sentinel) 값 전송 또는 생산자와 소비자 간에 합의된 예외 발생과 같은 다른 수단으로 더 잘 다루어집니다. 그러면 소비자는 센티넬 또는 예외를 감지하고 계산을 완료하고 정상적으로 반환함으로써 응답할 수 있습니다. 이러한 방식은 위임(delegation)이 있을 때 올바르게 동작합니다.
- 이 기능의 동기는 제너레이터로 보내지는 값 스트림의 끝을 제너레이터를 닫음으로써 알릴 수 있도록 하기 위함입니다. 제너레이터는
- 제안:
close()
가 값을 반환하지 않는 경우,None
이 아닌 값을 가진StopIteration
이 발생하면 예외를 발생시킵니다.- 해결: 그렇게 할 명확한 이유가 없습니다. 반환 값을 무시하는 것은 Python의 다른 곳에서는 오류로 간주되지 않습니다.
비판 (Criticisms)
이 제안에 따르면, yield from
표현식의 값은 일반 yield
표현식의 값과는 매우 다른 방식으로 파생됩니다. 이는 yield
라는 단어를 포함하지 않는 다른 문법이 더 적절할 수 있음을 시사하지만, 아직까지 수용 가능한 대안은 제안되지 않았습니다. 거부된 대안으로는 call
, delegate
, gcall
등이 있습니다.
서브제너레이터에서 return
이외의 다른 메커니즘을 사용하여 yield from
표현식으로 반환되는 값을 설정해야 한다는 제안이 있었습니다. 그러나 이는 서브제너레이터를 일시 중단 가능한 함수로 생각할 수 있다는 목표에 방해가 될 것입니다. 왜냐하면 다른 함수와 동일한 방식으로 값을 반환할 수 없기 때문입니다.
반환 값을 전달하기 위해 예외를 사용하는 것은 “예외 남용”으로 비판받았지만, 이 주장에 대한 구체적인 정당화는 없었습니다. 어쨌든, 이것은 단지 제안된 구현 중 하나일 뿐입니다. 제안의 본질적인 기능을 잃지 않고 다른 메커니즘을 사용할 수도 있습니다.
값을 반환하기 위해 StopIteration
대신 GeneratorReturn
과 같은 다른 예외를 사용해야 한다는 제안도 있었습니다. 그러나 이에 대한 설득력 있는 실제적인 이유는 제시되지 않았으며, StopIteration
에 value
속성을 추가한 것이 반환 값을 추출하는 데 발생할 수 있는 모든 어려움을 완화합니다. 또한, 다른 예외를 사용하면 일반 함수와 달리 제너레이터에서 값을 갖지 않는 return
이 return None
과 동등하지 않게 될 것입니다.
대안 제안 (Alternative Proposals)
이와 유사한 제안은 이전에도 있었으며, 일부는 yield from
대신 yield *
문법을 사용했습니다. yield *
는 더 간결하지만, 일반 yield
와 너무 비슷해 보이고 코드를 읽을 때 차이점을 간과할 수 있다는 주장이 있었습니다.
작성자의 지식으로는 이전 제안들은 값 yield에만 초점을 맞추었으며, 따라서 그들이 대체하는 두 줄짜리 for
루프가 새로운 문법을 정당화할 만큼 충분히 번거롭지 않다는 비판을 받았습니다. 이 제안은 전체 제너레이터 프로토콜을 다룸으로써 훨씬 더 많은 이점을 제공합니다.
추가 자료 (Additional Material)
제안된 문법 사용 예시와 위에 설명된 첫 번째 최적화를 기반으로 한 프로토타입 구현이 제공됩니다.
- 예시 및 구현: Python 3.3용으로 업데이트된 구현 버전은 트래커 이슈 #11682에서 확인할 수 있습니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인(public domain)으로 지정되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments