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일에 공식적으로 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 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이 발생할 경우 예외를 발생시키십시오.
결정: 그렇게 해야 할 분명한 이유가 없습니다. Python의 다른 어느 곳에서도 반환값을 무시하는 것을 오류로 간주하지 않습니다.
비판
이 제안에 따르면 yield from 표현식의 값은 일반 yield 표현식의 값과는 매우 다른 방식으로 도출됩니다. 이는 yield라는 단어를 포함하지 않는 다른 문법이 더 적절할 수 있음을 시사하지만, 지금까지 받아들일 만한 대안은 제안되지 않았습니다. 기각된 대안으로는 call, delegate, gcall이 있습니다.
yield from 표현식이 반환하는 값을 정하는 데 하위 제너레이터의 return 이외의 다른 메커니즘을 사용해야 한다는 제안이 있었습니다. 그러나 이렇게 하면 서브제너레이터가 다른 함수와 같은 방식으로 반환값을 돌려줄 수 없게 되므로, 서브제너레이터를 일시 중단 가능한 함수로 생각할 수 있게 한다는 목표에 방해가 됩니다.
반환값을 전달하는 데 예외를 사용하는 것은 “예외의 남용”이라는 비판을 받았지만, 이 주장에 대한 구체적인 근거는 제시되지 않았습니다. 어쨌든 이는 제안된 구현 방식 중 하나일 뿐이며, 제안의 본질적인 특징을 잃지 않고 다른 메커니즘을 사용할 수도 있습니다.
값을 반환하는 데 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.