PEP 380 – 서브제너레이터에 위임하기 위한 구문
- Author:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 13-Feb-2009
- Python-Version:
- 3.3
- Post-History:
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
제너레이터가 작업의 일부를 다른 제너레이터에 위임하도록 하는 구문을 제안합니다. 이를 통해 ‘yield’를 포함하는 코드의 일부를 분리하여 다른 제너레이터에 배치할 수 있습니다. 또한 서브제너레이터는 값을 반환할 수 있으며, 그 값을 위임하는 제너레이터에서 사용할 수 있습니다.
새로운 구문은 한 제너레이터가 다른 제너레이터가 생성한 값을 다시 yield할 때 몇 가지 최적화 기회도 제공합니다.
PEP 승인
Guido는 2011년 6월 26일 공식적으로 accepted the PEP을 수락했습니다.
동기
Python 제너레이터는 코루틴의 한 형태이지만, 직접 호출한 호출자에게만 값을 yield할 수 있다는 제한이 있습니다. 이는 yield을 포함하는 코드 일부를 다른 코드와 같은 방식으로 분리하여 별도의 함수에 넣을 수 없다는 의미입니다. 이렇게 분리하면 호출된 함수 자체가 제너레이터가 되며, 이 두 번째 제너레이터를 명시적으로 순회하면서 생성되는 모든 값을 다시 yield해야 합니다.
값의 yield만 고려한다면 다음과 같은 루프를 사용하여 큰 어려움 없이 수행할 수 있습니다.
for v in g:
yield v
그러나 send(), throw() 및 close() 호출 시 서브제너레이터가 호출자와 올바르게 상호작용하도록 하려면 일이 훨씬 더 어려워집니다. 뒤에서 살펴보겠지만 필요한 코드는 매우 복잡하며, 모든 예외적인 경우를 올바르게 처리하기도 까다롭습니다.
이 문제를 해결하기 위한 새로운 구문을 제안합니다. 가장 단순한 사용 사례에서는 위의 for 루프와 동등하지만, 제너레이터 동작의 전체 범위도 처리하며 제너레이터 코드를 간단하고 직관적인 방식으로 리팩터링할 수 있도록 합니다.
제안
제너레이터 본문에서 다음과 같은 새로운 표현식 구문을 사용할 수 있습니다.
yield from <expr>
여기서 <expr>은 이터러블로 평가되는 표현식이며, 이터러블에서 이터레이터를 추출합니다. 이터레이터가 소진될 때까지 실행되며, 그동안 yield from표현식을 포함하는 제너레이터(“위임하는 제너레이터”)의 호출자와 직접 값을 주고받습니다.
또한 이터레이터가 다른 제너레이터인 경우 서브제너레이터는 값을 포함하는 return문을 실행할 수 있으며, 해당 값은 yield from표현식의 값이 됩니다.
yield from표현식의 전체 의미는 다음과 같이 제너레이터 프로토콜의 관점에서 설명할 수 있습니다.
- 이터레이터가 yield하는 모든 값은 호출자에게 직접 전달됩니다.
send()를 사용하여 위임하는 제너레이터에 전달된 모든 값은 이터레이터에 직접 전달됩니다. 전달된 값이 None이면 이터레이터의__next__()메서드가 호출됩니다. 전달된 값이 None이 아니면 이터레이터의send()메서드가 호출됩니다. 호출에서 StopIteration이 발생하면 위임하는 제너레이터가 재개됩니다. 그 밖의 모든 예외는 위임하는 제너레이터로 전파됩니다.- 위임하는 제너레이터에 던져진 GeneratorExit 이외의 예외는 이터레이터의
throw()메서드에 전달됩니다. 호출에서 StopIteration이 발생하면 위임하는 제너레이터가 재개됩니다. 그 밖의 모든 예외는 위임하는 제너레이터로 전파됩니다. - GeneratorExit 예외가 위임하는 제너레이터에 던져지거나 위임하는 제너레이터의
close()메서드가 호출되면, 이터레이터에close()메서드가 있는 경우 해당 메서드가 호출됩니다. 이 호출로 인해 예외가 발생하면 해당 예외가 위임하는 제너레이터로 전파됩니다. 그렇지 않으면 GeneratorExit가 위임하는 제너레이터에서 발생합니다. yield from표현식의 값은 이터레이터가 종료될 때 발생시키는StopIteration예외의 첫 번째 인자입니다.- 제너레이터에서
return expr을 실행하면 제너레이터가 종료될 때StopIteration(expr)이 발생합니다.
StopIteration 개선
편의를 위해 StopIteration 예외에는 첫 번째 인자를 보유하는 value 속성이 제공되며, 인자가 없으면 None이 해당 속성에 저장됩니다.
형식적 의미론
이 절에서는 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)
근거
리팩터링 원칙
위에서 제시한 의미론 대부분의 근거는 제너레이터 코드를 리팩터링할 수 있도록 하려는 요구에서 비롯됩니다. 하나 이상의 yield 표현식을 포함하는 코드 일부를 별도의 함수로 옮긴 다음(주변 스코프의 변수 참조를 처리하는 일반적인 기법 등을 사용하여), yield from 표현식을 사용해 새 함수를 호출할 수 있어야 합니다.
그 결과로 생성되는 복합 제너레이터의 동작은 합리적으로 가능한 범위에서 모든 상황에서, __next__()를 호출하거나 send()를 호출하거나 throw()를 호출하거나 close()를 호출하는 경우를 포함하여, 원래 분리되지 않은 제너레이터와 동일해야 합니다.
제너레이터가 아닌 서브 이터레이터의 경우에도 제너레이터의 경우를 합리적으로 일반화하도록 의미론을 선택했습니다.
제안된 의미론에는 리팩터링과 관련하여 다음과 같은 제한이 있습니다.
- GeneratorExit를 포착한 후 다시 발생시키지 않는 코드 블록은 동작을 정확히 동일하게 유지하면서 분리해 낼 수 없습니다.
- 위임하는 제너레이터에 StopIteration 예외가 던져지는 경우, 분리된 코드가 분리되지 않은 코드와 동일하게 동작하지 않을 수 있습니다.
이러한 사용 사례는 드물거나 사실상 존재하지 않으므로, 이를 지원하는 데 필요한 추가 복잡성은 감수할 가치가 없다고 판단했습니다.
종료 처리
위임하는 제너레이터가 yield from에서 일시 중단된 동안 해당 제너레이터의 close() 메서드를 호출하여 명시적으로 종료할 때 서브 이터레이터도 종료해야 하는지를 두고 약간의 논의가 있었습니다. 그렇게 하지 않아야 한다는 주장은 서브 이터레이터에 대한 참조가 다른 곳에 존재하는 경우 서브 이터레이터가 너무 일찍 종료되는 결과를 초래한다는 것입니다.
참조 카운팅을 사용하지 않는 Python 구현을 고려한 결과, 이 명시적 종료를 수행하기로 결정했습니다. 이렇게 하면 모든 Python 구현에서 분리된 제너레이터를 명시적으로 닫는 것이 분리되지 않은 제너레이터를 닫는 것과 동일한 효과를 냅니다.
대부분의 사용 사례에서 서브 이터레이터는 공유되지 않는다고 가정합니다. 공유된 서브 이터레이터라는 드문 경우에는 throw() 및 close() 호출을 차단하는 래퍼를 사용하거나, 서브 이터레이터를 호출할 때 yield from 이외의 방법을 사용하여 처리할 수 있습니다.
스레드로서의 제너레이터
제너레이터가 값을 반환할 수 있어야 하는 동기 중 하나는 제너레이터를 사용하여 경량 스레드를 구현하는 것과 관련됩니다. 이러한 방식으로 제너레이터를 사용할 때는 경량 스레드가 수행하는 계산을 여러 함수에 분산하고 싶어 하는 것이 합리적입니다. 서브제너레이터를 일반 함수인 것처럼 호출하여 매개변수를 전달하고 반환된 값을 받을 수 있기를 원하게 됩니다.
제안된 구문을 사용하면 다음과 같은 문을
y = f(x)
여기서 f가 일반 함수인 경우, 위임 호출로 변환할 수 있습니다.
y = yield from g(x)
여기서 g는 제너레이터입니다. 결과 코드의 동작은 g를 yield 문을 사용하여 일시 중지할 수 있는 일반 함수로 생각하면 추론할 수 있습니다.
이러한 방식으로 제너레이터를 스레드로 사용할 때는 일반적으로 yield에서 전달되거나 yield로 전달되는 값에는 관심이 없습니다. 그러나 스레드를 항목의 생산자 또는 소비자로 보는 경우처럼, 이 기능에도 사용 사례가 있습니다. yield from 표현식을 사용하면 스레드의 논리를 원하는 만큼 많은 함수에 분산할 수 있으며, 항목의 생산 또는 소비는 모든 하위 함수에서 발생할 수 있고, 항목은 최종 출처 또는 목적지로부터 또는 그쪽으로 자동으로 전달됩니다.
throw() 및 close()와 관련해서는, 외부에서 스레드로 예외가 전달되면 스레드가 일시 중지된 가장 안쪽 제너레이터에서 먼저 발생한 다음 그 지점에서 바깥쪽으로 전파되고, 외부에서 close()를 호출하여 스레드를 종료하면 활성 제너레이터의 연쇄가 가장 안쪽에서 바깥쪽 순서로 마무리된다고 예상하는 것이 합리적입니다.
구문
제안된 특정 구문은 새로운 키워드를 도입하지 않으면서 그 의미를 암시하고 일반적인 yield와 다르다는 점을 명확히 드러내도록 선택되었습니다.
최적화
특수 구문을 사용하면 제너레이터의 연쇄가 긴 경우 최적화할 수 있는 가능성이 열립니다. 예를 들어 트리 구조를 재귀적으로 순회할 때 이러한 연쇄가 발생할 수 있습니다. __next__() 호출과 yield된 값을 연쇄 아래쪽과 위쪽으로 전달하는 오버헤드로 인해 원래 O(n)이어야 하는 연산이 최악의 경우 O(n**2)가 될 수 있습니다.
가능한 전략은 위임할 제너레이터를 보관하는 슬롯을 제너레이터 객체에 추가하는 것입니다. 제너레이터에서 __next__() 또는 send() 호출이 이루어지면 이 슬롯을 먼저 확인하고, 비어 있지 않으면 해당 슬롯이 참조하는 제너레이터를 대신 재개합니다. 해당 제너레이터가 StopIteration을 발생시키면 슬롯을 비우고 주 제너레이터를 재개합니다.
이렇게 하면 Python 코드 실행이 전혀 포함되지 않는 C 함수 호출의 연쇄로 위임 오버헤드를 줄일 수 있습니다. 가능한 개선 방법은 전체 제너레이터 연쇄를 루프에서 순회하여 끝에 있는 제너레이터를 직접 재개하는 것이지만, 그러면 StopIteration 처리가 더 복잡해집니다.
값을 반환하기 위한 StopIteration 사용
제너레이터의 반환값을 전달하는 방법은 여러 가지가 있을 수 있습니다. 대안으로는 제너레이터-이터레이터 객체의 속성으로 저장하거나, 서브제너레이터에 대한 close() 호출의 값으로 반환하는 방법 등이 있습니다. 그러나 제안된 메커니즘은 몇 가지 이유로 매력적입니다.
- StopIteration 예외를 일반화하여 사용하면 다른 종류의 이터레이터도 추가 속성이나 close() 메서드를 늘리지 않고 프로토콜에 쉽게 참여할 수 있습니다.
- 서브제너레이터의 반환값을 사용할 수 있게 되는 시점이 예외가 발생하는 시점과 동일하므로 구현이 단순해집니다. 이후의 어느 시점까지 지연하려면 반환값을 어딘가에 저장해야 합니다.
거부된 아이디어
몇 가지 아이디어가 논의되었지만 거부되었습니다.
제안: 초기 __next__() 호출을 방지하거나, 지정된 값을 사용하는 send() 호출로 대체할 방법이 있어야 합니다. 이는 초기 __next__()가 자동으로 수행되도록 래핑된 제너레이터의 사용을 지원하려는 것입니다.
결정: 이 제안의 범위를 벗어납니다. 그런 제너레이터는 yield from과 함께 사용해서는 안 됩니다.
제안: 하위 이터레이터를 닫을 때 값이 있는 StopIteration이 발생하면, 위임하는 제너레이터에 대한 close() 호출에서 그 값을 반환하십시오.
이 기능의 동기는 제너레이터로 전송되는 값들의 스트림의 끝을 그 제너레이터를 닫음으로써 알릴 수 있도록 하려는 것입니다. 제너레이터는 GeneratorExit을 잡아서 계산을 마치고 결과를 반환하며, 이 결과는 close() 호출의 반환값이 됩니다.
결정: close()와 GeneratorExit의 이러한 사용법은 중단 및 정리 메커니즘이라는 이들의 현재 역할과 양립할 수 없습니다. 이는 위임하는 제너레이터를 닫을 때, 하위 제너레이터가 닫힌 후 GeneratorExit을 다시 발생시키는 대신 위임하는 제너레이터를 재개할 것을 요구합니다. 하지만 이는 받아들일 수 없습니다. 정리 목적으로 close()가 호출되는 경우 위임하는 제너레이터가 제대로 마무리되도록 보장하지 못하기 때문입니다.
소비자에게 값의 끝을 알리는 것은 센티널 값을 전송하거나 생산자와 소비자가 합의한 예외를 던져 넣는 등 다른 방법으로 처리하는 것이 더 낫습니다. 그러면 소비자는 센티널이나 예외를 감지하고 계산을 마치고 정상적으로 반환함으로써 응답할 수 있습니다. 이러한 방식은 위임이 있는 경우에도 올바르게 동작합니다.
제안: close()가 값을 반환하지 않는다면, None이 아닌 값을 가진 StopIteration이 발생할 경우 예외를 발생시키십시오.
결정: 그렇게 해야 할 명확한 이유가 없습니다. 반환값을 무시하는 것은 파이썬의 다른 어디에서도 오류로 간주되지 않습니다.
비판
이 제안 하에서 yield from 표현식의 값은 일반 yield 표현식의 값과는 매우 다른 방식으로 도출됩니다. 이는 yield라는 단어를 포함하지 않는 다른 구문이 더 적합할 수 있음을 시사하지만, 지금까지 받아들일 만한 대안은 제안되지 않았습니다. 거부된 대안으로는 call, delegate, gcall이 있습니다.
하위 제너레이터에서 return이 아닌 다른 메커니즘을 사용해 yield from 표현식이 반환하는 값을 정해야 한다는 제안이 있었습니다. 하지만 이는 하위 제너레이터를 중단 가능한 함수로 여길 수 있게 한다는 목표를 저해합니다. 다른 함수들과 같은 방식으로 값을 반환할 수 없게 되기 때문입니다.
반환값을 전달하기 위해 예외를 사용하는 것은 이 주장에 대한 구체적인 근거 없이 “예외의 남용”이라고 비판받아 왔습니다. 어쨌든 이는 제안된 구현 방법 중 하나일 뿐이며, 이 제안의 필수적인 특징을 잃지 않고도 다른 메커니즘을 사용할 수 있습니다.
값을 반환하기 위해 StopIteration 대신 GeneratorReturn 같은 다른 예외를 사용해야 한다는 제안이 있었습니다. 하지만 이에 대한 설득력 있는 실질적 이유는 제시되지 않았으며, StopIteration에 value 속성을 추가함으로써 값을 가질 수도, 가지지 않을 수도 있는 StopIteration 예외에서 반환값을 추출하는 어려움이 완화됩니다. 또한 다른 예외를 사용하면, 일반 함수와는 달리 제너레이터에서 값 없는 ‘return’이 ‘return None’과 동등하지 않게 됩니다.
대안 제안
비슷한 취지의 제안이 이전에도 있었으며, 그중 일부는 yield from 대신 yield * 구문을 사용했습니다. yield *가 더 간결하기는 하지만, 일반 yield와 너무 비슷해 보여서 코드를 읽을 때 그 차이가 간과될 수 있다는 주장도 있을 수 있습니다.
저자가 아는 한, 기존 제안들은 값을 yield하는 것에만 초점을 맞추어 왔으며, 그 때문에 그것들이 대체하는 두 줄짜리 for 루프가 새로운 문법을 정당화할 만큼 충분히 번거롭지 않다는 비판을 받아 왔습니다. 전체 제너레이터 프로토콜을 다룸으로써, 이 제안은 상당히 더 많은 이점을 제공합니다.
추가 자료
제안된 문법의 사용 예시 몇 가지와, 위에서 설명한 첫 번째 최적화를 기반으로 한 프로토타입 구현을 확인할 수 있습니다.
Python 3.3에 맞게 업데이트된 구현 버전은 트래커의 issue #11682에서 확인할 수 있습니다.
Copyright
This document has been placed in the public domain.