PEP 342 – 향상된 제너레이터를 통한 코루틴
- Author:
- Guido van Rossum, Phillip J. Eby
- Status:
- Final
- Type:
- Standards Track
- Created:
- 10-May-2005
- Python-Version:
- 2.5
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
서론
이 PEP는 제너레이터의 API와 구문을 일부 개선하여 간단한 코루틴으로 사용할 수 있도록 제안합니다. 기본적으로 다음 두 PEP의 아이디어를 결합한 것이며, 이 PEP가 채택되면 중복된 것으로 간주될 수 있습니다.
동기
코루틴은 시뮬레이션, 게임, 비동기 I/O 및 기타 이벤트 기반 프로그래밍이나 협력적 멀티태스킹과 같은 여러 알고리즘을 표현하는 자연스러운 방법입니다. Python의 제너레이터 함수는 값을 생성하기 위해 실행을 일시 중지할 수 있다는 점에서 거의 코루틴에 가깝지만, 실행이 재개될 때 값이나 예외를 전달할 방법은 제공하지 않습니다. 또한 try/finally 블록의 try 부분 안에서는 실행을 일시 중지할 수 없으므로, 중단된 코루틴이 스스로 정리 작업을 수행하기 어렵습니다.
또한 다른 함수가 실행 중일 때 제너레이터는 제어권을 양보할 수 없습니다. 해당 함수 자체가 제너레이터로 표현되고, 바깥 제너레이터가 안쪽 제너레이터가 양보한 값에 응답하여 값을 양보하도록 작성된 경우는 예외입니다. 이 때문에 비동기 통신과 같이 비교적 단순한 사용 사례조차 구현하기가 복잡해집니다. 함수를 호출하려면 제너레이터가 블록되어 제어권을 양보할 수 없게 되거나, 필요한 모든 함수 호출 주위에 많은 반복문 보일러플레이트 코드를 추가해야 하기 때문입니다.
그러나 제너레이터가 일시 중지된 지점에서 제너레이터 안으로 값이나 예외를 전달할 수 있다면, 간단한 코루틴 스케줄러 또는 트램펄린 함수를 통해 코루틴이 블록 없이 서로를 호출할 수 있게 됩니다. 이는 비동기 애플리케이션에 엄청난 이점이 됩니다. 그러면 이러한 애플리케이션은 데이터가 전송되거나 사용 가능해질 때까지 I/O 스케줄러에 제어권을 양보하여 비블로킹 소켓 I/O를 수행하는 코루틴을 작성할 수 있습니다. 한편 I/O를 수행하는 코드는 단순히 다음과 같이 작성하면 됩니다.:
data = (yield nonblocking_read(my_socket, nbytes))
nonblocking_read() 코루틴이 값을 생성할 때까지 실행을 일시 중지하려면 다음과 같이 합니다.
즉, 언어와 제너레이터-이터레이터 형식의 구현을 약간만 개선하면 Python은 전체 애플리케이션을 콜백 시퀀스로 작성하지 않고도 비동기 작업을 수행할 수 있으며, 수백 또는 수천 개의 협력적 멀티태스킹 의사 스레드가 필요한 프로그램에서 리소스를 많이 사용하는 스레드를 사용하지 않아도 됩니다. 따라서 이러한 개선을 통해 표준 Python은 CPython 코어 또는 해당 API를 크게 수정하지 않고도 Stackless Python 포크의 여러 이점을 얻을 수 있습니다. 또한 이러한 개선은 이미 제너레이터를 지원하는 모든 Python 구현(예: Jython)에서 쉽게 구현할 수 있어야 합니다.
사양 요약
제너레이터-이터레이터 형식에 몇 가지 간단한 메서드를 추가하고 구문을 조금만 조정하면, Python 개발자는 제너레이터 함수를 사용하여 코루틴과 기타 형태의 협력적 멀티태스킹을 구현할 수 있습니다. 이러한 메서드와 조정 사항은 다음과 같습니다.
yield를 문장이 아니라 표현식으로 재정의합니다. 현재의 yield 문은 그 값이 버려지는 yield 표현식이 됩니다. 제너레이터가 일반적인next()호출로 재개될 때 yield 표현식의 값은 항상None입니다.- 제너레이터-이터레이터에 새로운
send()메서드를 추가합니다. 이 메서드는 제너레이터를 재개하고 현재 yield 표현식의 결과가 되는 값을 전송합니다.send()메서드는 제너레이터가 양보한 다음 값을 반환하거나, 제너레이터가 다른 값을 양보하지 않고 종료되면StopIteration을 발생시킵니다. - 제너레이터-이터레이터에 새로운
throw()메서드를 추가합니다. 이 메서드는 제너레이터가 일시 중지된 지점에서 예외를 발생시키고, 제너레이터가 양보한 다음 값을 반환하거나 다른 값을 양보하지 않고 종료되면StopIteration을 발생시킵니다. (제너레이터가 전달된 예외를 포착하지 않거나 다른 예외를 발생시키면 해당 예외가 호출자에게 전파됩니다.) - 제너레이터-이터레이터에 제너레이터가 일시 중지된 지점에서
GeneratorExit를 발생시키는close()메서드를 추가합니다. 그 후 제너레이터가StopIteration(정상적으로 종료하거나 이미 닫혀 있기 때문) 또는GeneratorExit(예외를 잡지 않기 때문)를 발생시키면close()는 호출자에게 반환됩니다. 제너레이터가 값을 산출하면RuntimeError가 발생합니다. 제너레이터가 다른 예외를 발생시키면 호출자에게 전파됩니다. 제너레이터가 예외 또는 정상적인 종료로 이미 종료된 경우close()는 아무 작업도 수행하지 않습니다. - 제너레이터 이터레이터가 가비지 컬렉션될 때
close()가 호출되도록 지원을 추가합니다. - 가비지 컬렉션 또는 명시적인
close()호출로 이제finally절을 실행할 수 있으므로try/finally블록에서yield를 사용할 수 있도록 허용합니다.
현재 Python CVS HEAD를 대상으로 이러한 모든 변경 사항을 구현한 프로토타입 패치를 SourceForge 패치 #1223381(https://bugs.python.org/issue1223381)로 사용할 수 있습니다.
사양: 제너레이터로 값 보내기
새로운 제너레이터 메서드: send(value)
제너레이터-이터레이터를 위한 send()라는 새 메서드를 제안합니다. 이 메서드는 제너레이터에 보내질 값인 인수를 정확히 하나 받습니다. send(None)를 호출하는 것은 제너레이터의 next() 메서드를 호출하는 것과 정확히 같습니다. send()를 다른 값과 함께 호출하는 것도 동일하지만, 제너레이터의 현재 yield 표현식이 생성하는 값은 달라집니다.
제너레이터-이터레이터는 제너레이터 함수 본문의 맨 앞에서 실행을 시작하므로, 제너레이터가 막 생성된 시점에는 값을 받을 yield 표현식이 없습니다. 따라서 제너레이터 이터레이터가 막 시작된 상태에서는 None이 아닌 인수와 함께 send()를 호출할 수 없으며, 그렇게 하면 (아마도 어떤 종류의 논리 오류 때문이겠지만) TypeError가 발생합니다. 그러므로 코루틴과 통신하려면 먼저 next()를 호출하거나 send(None)을 호출하여 실행을 첫 번째 yield 표현식까지 진행해야 합니다.
next() 메서드와 마찬가지로 send() 메서드는 제너레이터-이터레이터가 산출하는 다음 값을 반환하며, 제너레이터가 정상적으로 종료되었거나 이미 종료된 경우에는 StopIteration을 발생시킵니다. 제너레이터가 잡히지 않은 예외를 발생시키면 해당 예외가 send()의 호출자에게 전파됩니다.
새로운 구문: Yield 표현식
yield 문을 대입문의 오른쪽에서 사용할 수 있도록 허용하며, 이 경우 이를 yield 표현식이라고 합니다. 이 yield 표현식의 값은 send()가 None이 아닌 인수와 함께 호출되지 않는 한 None입니다. 아래를 참조하십시오.
yield 표현식은 대입문의 오른쪽에 있는 최상위 표현식으로 나타나는 경우를 제외하고는 항상 괄호로 묶어야 합니다. 따라서
x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)
모두 올바르지만,
x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)
모두 올바르지 않습니다. (일부 경계 사례는 현재 yield 12, 42가 허용된다는 점에서 비롯됩니다.)
표현식 없이 사용하는 yield 문 또는 yield 표현식도 이제 허용된다는 점에 유의하십시오. 이는 타당합니다. next() 호출에서 정보 흐름이 반전되면 명시적인 값을 전달하지 않고도 값을 산출할 수 있어야 하기 때문입니다(yield는 물론 yield None과 같습니다).
send(value)를 호출하면, 해당 호출로 재개되는 yield 표현식이 전달된 값을 반환합니다. next()를 호출하면 재개된 yield 표현식이 None을 반환합니다. yield 표현식이 yield 문인 경우 이 반환된 값은 무시되며, 이는 문으로 사용된 함수 호출이 반환하는 값을 무시하는 것과 유사합니다.
사실상 yield 표현식은 반전된 함수 호출과 같습니다. yield에 대한 인수는 실제로 현재 실행 중인 함수에서 반환(산출)되며, 반환 값은 send()를 통해 전달된 인수입니다.
참고: yield에 대한 구문 확장으로 인해 그 사용법이 Ruby에서의 사용법과 매우 유사합니다. 이는 의도된 것입니다. Python에서는 블록이 return EXPR이 아니라 send(EXPR)를 사용하여 제너레이터에 값을 전달하며, 제너레이터와 블록 사이에서 제어가 전달되는 기본 메커니즘은 완전히 다르다는 점에 유의하십시오. Python의 블록은 썽크로 컴파일되지 않습니다. 대신 yield가 제너레이터 프레임의 실행을 일시 중단합니다. 일부 예외적인 경우에는 다르게 작동합니다. Python에서는 나중에 사용하기 위해 블록을 저장할 수 없으며, 블록이 있는지 여부를 검사할 수도 없습니다. (XXX - 이제 블록에 관한 이 내용은 문맥에 맞지 않는 것 같습니다. 아마 Guido가 명확하게 수정할 수 있을 것입니다.)
사양: 예외 및 정리
제너레이터 함수를 호출하여 생성되는 이터레이터를 제너레이터 객체라고 합니다. 아래에서 g는 항상 제너레이터 객체를 가리킵니다.
새로운 구문: try-finally 내부에서 yield 허용
제너레이터 함수의 구문이 확장되어 try-finally 문 내부에 yield 문을 사용할 수 있습니다.
새로운 제너레이터 메서드: throw(type, value=None, traceback=None)
g.throw(type, value, traceback)은 제너레이터 g가 현재 일시 중단된 지점, 즉 yield 문에서 또는 아직 next()가 호출되지 않았다면 함수 본문의 시작 지점에서 지정된 예외를 발생시킵니다. 제너레이터가 예외를 포착하고 다른 값을 yield하면 그것이 g.throw()의 반환 값입니다. 제너레이터가 예외를 포착하지 않으면 throw()는 전달받은 동일한 예외를 발생시키는 것처럼 동작합니다(예외가 그대로 전파됩니다). 제너레이터가 다른 예외를 발생시키면(여기에는 제너레이터가 반환할 때 생성되는 StopIteration도 포함됩니다) 해당 예외가 throw() 호출에 의해 발생합니다. 요약하면 throw()는 일시 중단 지점에서 예외를 발생시킨다는 점을 제외하면 next() 또는 send()처럼 동작합니다. 제너레이터가 이미 닫힌 상태라면 throw()는 제너레이터 코드 중 어떤 것도 실행하지 않고 전달받은 예외를 그대로 발생시킵니다.
예외를 발생시킨 효과는 다음 문이:
raise type, value, traceback
일시 중단 지점에서 실행된 것과 정확히 같습니다. type 인자는 None이어서는 안 되며, type과 value는 호환되어야 합니다. value가 type의 인스턴스가 아니면 raise 문이 예외 인스턴스를 생성할 때 사용하는 것과 동일한 규칙에 따라 value를 사용하여 새 예외 인스턴스가 생성됩니다. traceback이 제공되는 경우 유효한 Python traceback 객체여야 하며, 그렇지 않으면 TypeError가 발생합니다.
참고: throw() 메서드의 이름은 여러 이유로 선택되었습니다. Raise는 키워드이므로 메서드 이름으로 사용할 수 없습니다. 현재 실행 지점에서 즉시 예외를 발생시키는 raise와 달리 throw()는 먼저 제너레이터를 재개한 다음에야 예외를 발생시킵니다. throw라는 단어는 예외를 다른 위치에 넣는다는 의미를 암시하며, 이미 다른 언어에서 예외와 관련되어 사용되고 있습니다.
대체 메서드 이름으로 resolve(), signal(), genraise(), raiseinto(), flush()가 검토되었습니다. 이 중 어느 것도 throw()만큼 적합해 보이지 않습니다.
새로운 표준 예외: GeneratorExit
GeneratorExit라는 새로운 표준 예외가 정의되며, Exception을 상속합니다. 제너레이터는 이를 다시 발생시키거나(또는 단순히 포착하지 않거나) StopIteration을 발생시켜 처리해야 합니다.
새로운 제너레이터 메서드: close()
g.close()는 다음 의사 코드로 정의됩니다.:
def close(self):
try:
self.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
pass
else:
raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught
새 제너레이터 메서드: __del__()
g.__del__()은 g.close()의 래퍼입니다. 제너레이터 객체가 가비지 컬렉션될 때 호출됩니다(CPython에서는 참조 횟수가 0이 될 때입니다). close()가 예외를 발생시키면 해당 예외의 트레이스백이 sys.stderr에 출력된 후 무시되며, 가비지 컬렉션을 유발한 위치로 전파되지 않습니다. 이는 클래스 인스턴스의 __del__() 메서드에서 예외를 처리하는 방식과 일관됩니다.
제너레이터 객체가 순환에 참여하면 g.__del__()이 호출되지 않을 수 있습니다. 이것은 현재 CPython 가비지 컬렉터의 동작입니다. 이러한 제한이 있는 이유는 GC 코드가 순환을 수집하려면 임의의 지점에서 순환을 끊어야하며, 그 이후에는 해당 순환을 구성하던 객체가 유효하지 않은 상태일 수 있으므로 어떤 Python 코드도 이를 볼 수 없어야 하기 때문입니다. 순환에 매달려 있는 객체에는 이러한 제한이 적용되지 않습니다.
실제로 제너레이터 객체가 순환에 참여하는 경우는 드물다는 점에 유의하십시오. 그러나 제너레이터 객체를 전역 변수에 저장하면 제너레이터 프레임의 f_globals 포인터를 통해 순환이 생성됩니다. 순환을 생성하는 또 다른 방법은 제너레이터에 인자로 전달되는 데이터 구조에 제너레이터 객체에 대한 참조를 저장하는 것입니다(예를 들어, 어떤 객체에 제너레이터인 메서드가 있고 해당 메서드가 생성한 실행 중인 이터레이터에 대한 참조를 유지하는 경우입니다). 제너레이터를 사용하는 일반적인 패턴을 고려하면 이 두 경우 모두 발생할 가능성은 매우 낮습니다.
또한 이 PEP의 CPython 구현에서는 오류 또는 정상 종료로 인해 제너레이터의 실행이 종료될 때마다 제너레이터가 사용하는 프레임 객체를 해제해야 합니다. 이렇게 하면 재개할 수 없는 제너레이터가 수집할 수 없는 참조 순환의 일부로 남지 않습니다. 이를 통해 다른 코드에서 close()를 try/finally 또는 with 블록에서 잠재적으로 사용하여 주어진 제너레이터가 적절히 종료되도록 할 수 있습니다(PEP 343에 따름).
선택적 확장 기능
확장된 continue 문
이 PEP의 이전 초안에서는 for 루프에서 사용할 새로운 continue EXPR 구문을 제안했으며(PEP 340에서 가져옴), 이를 통해 EXPR의 값을 반복 중인 이터레이터에 전달하도록 했습니다. 이 기능은 당분간 철회되었습니다. 이 PEP의 범위가 제너레이터-이터레이터에 값을 전달하는 데만 초점을 맞추도록 좁혀졌으며, 다른 종류의 이터레이터는 대상으로 하지 않기 때문입니다. 또한 Python-Dev 목록의 일부 사람들은 이 특정 기능을 위해 새 구문을 추가하는 것이 기껏해야 시기상조라고 생각했습니다.
미해결 문제
python-dev에서의 논의를 통해 몇 가지 미해결 문제가 드러났습니다. 여기에서는 제가 선호하는 해결 방안과 그 동기를 함께 나열합니다. 현재 작성된 PEP에는 이러한 선호하는 해결 방안이 반영되어 있습니다.
- 제너레이터가
GeneratorExit예외에 대한 응답으로 또 다른 값을 산출할 때close()는 어떤 예외를 발생시켜야 합니까?저는 처음에
TypeError를 선택했습니다. 이는 제너레이터 함수의 심각한 오작동을 나타내며, 코드를 변경하여 수정해야 하기 때문입니다. 그러나 PEP 343의with_template데코레이터 클래스는 유사한 위반에RuntimeError를 사용합니다. 타당하게 말하면 모두 동일한 예외를 사용해야 합니다. 저는 이 목적만을 위한 새 예외 클래스를 도입하고 싶지는 않습니다. 사람들이 잡기를 원하는 예외가 아니기 때문입니다. 이 예외가 프로그래머에게 보이는 트레이스백으로 변환되어 프로그래머가 코드를 수정하기를 원합니다. 따라서 이제 두 경우 모두RuntimeError를 발생시켜야 한다고 생각합니다. 여기에는 몇 가지 선례가 있습니다. 무한 재귀가 감지되는 상황과 초기화되지 않은 객체, 그리고 다양한 기타 조건에서 핵심 Python 코드가 이를 발생시킵니다. - Oren Tirosh는 consumer interface와의 호환성을 위해
send()메서드의 이름을feed()로 바꿀 것을 제안했습니다(자세한 명세는 http://effbot.org/zone/consumer.htm 을 참고하십시오).하지만 consumer interface를 더 자세히 살펴보면,
feed()에 원하는 의미가send()와는 다른 것으로 보이는데, 이는send()는 막 시작된 제너레이터에서 의미 있게 호출될 수 없기 때문입니다. 또한, 현재 정의된 consumer interface는StopIteration에 대한 처리를 포함하고 있지 않습니다.따라서, 제너레이터 함수를 감싸서 consumer interface를 따르도록 만드는 간단한 데코레이터를 만드는 것이 아마 더 유용할 것으로 보입니다. 예를 들어, 초기
next()호출로 제너레이터를 워밍업하고, StopIteration을 잡아내며, 어쩌면 제너레이터 함수를 다시 호출하여reset()까지 제공할 수 있을 것입니다.
예제
- 제너레이터 함수가 처음 호출될 때 자동으로 첫 번째 yield 지점까지 진행하도록 만드는 간단한 consumer 데코레이터:
def consumer(func): def wrapper(*args,**kw): gen = func(*args, **kw) gen.next() return gen wrapper.__name__ = func.__name__ wrapper.__dict__ = func.__dict__ wrapper.__doc__ = func.__doc__ return wrapper
- 이미지를 받아 섬네일 페이지를 만들고 이를 다른 consumer에 전달하는 역방향 제너레이터를 생성하기 위해 consumer 데코레이터를 사용하는 예제입니다. 이와 같은 함수들은 서로 연결되어, 각각 복잡한 내부 상태를 가질 수 있는 consumer들의 효율적인 처리 파이프라인을 형성할 수 있습니다:
@consumer def thumbnail_pager(pagesize, thumbsize, destination): while True: page = new_image(pagesize) rows, columns = pagesize / thumbsize pending = False try: for row in xrange(rows): for column in xrange(columns): thumb = create_thumbnail((yield), thumbsize) page.write( thumb, col*thumbsize.x, row*thumbsize.y ) pending = True except GeneratorExit: # close() was called, so flush any pending output if pending: destination.send(page) # then close the downstream consumer, and exit destination.close() return else: # we finished a page full of thumbnails, so send it # downstream and keep on looping destination.send(page) @consumer def jpeg_writer(dirname): fileno = 1 while True: filename = os.path.join(dirname,"page%04d.jpg" % fileno) write_jpeg((yield), filename) fileno += 1 # Put them together to make a function that makes thumbnail # pages from a list of images and other parameters. # def write_thumbnails(pagesize, thumbsize, images, output_dir): pipeline = thumbnail_pager( pagesize, thumbsize, jpeg_writer(output_dir) ) for image in images: pipeline.send(image) pipeline.close()
- 코루틴이 호출하고자 하는 코루틴을 yield함으로써 다른 코루틴을 호출할 수 있게 하는 간단한 코루틴 스케줄러, 즉 트램폴린입니다. 코루틴이 yield한, 제너레이터가 아닌 값은 그 값을 yield한 코루틴을 호출한 코루틴에게 반환됩니다. 마찬가지로 코루틴이 예외를 발생시키면 그 예외는 호출자에게 전파됩니다. 사실상 이 예제는, 그렇지 않았다면 블로킹되었을 루틴을 호출하기 위해 yield 표현식을 사용하는 한, Stackless Python에서 사용되는 것과 같은 간단한 태스크릿(tasklet)을 흉내 냅니다. 이것은 아주 간단한 예시일 뿐이며, 훨씬 더 정교한 스케줄러도 가능합니다. (예를 들어 기존의 Python용 GTasklet 프레임워크(http://www.gnome.org/~gjc/gtasklet/gtasklets.html)와 peak.events 프레임워크(http://peak.telecommunity.com/)는 이미 유사한 스케줄링 기능을 구현하고 있지만, 제너레이터에 값이나 예외를 전달할 수 없다는 문제 때문에 현재는 어색한 우회 방법을 사용해야 합니다.)
import collections class Trampoline: """Manage communications between coroutines""" running = False def __init__(self): self.queue = collections.deque() def add(self, coroutine): """Request that a coroutine be executed""" self.schedule(coroutine) def run(self): result = None self.running = True try: while self.running and self.queue: func = self.queue.popleft() result = func() return result finally: self.running = False def stop(self): self.running = False def schedule(self, coroutine, stack=(), val=None, *exc): def resume(): value = val try: if exc: value = coroutine.throw(value,*exc) else: value = coroutine.send(value) except: if stack: # send the error back to the "caller" self.schedule( stack[0], stack[1], *sys.exc_info() ) else: # Nothing left in this pseudothread to # handle it, let it propagate to the # run loop raise if isinstance(value, types.GeneratorType): # Yielded to a specific coroutine, push the # current one on the stack, and call the new # one with no args self.schedule(value, (coroutine,stack)) elif stack: # Yielded a result, pop the stack and send the # value to the caller self.schedule(stack[0], stack[1], value) # else: this pseudothread has ended self.queue.append(resume)
- 간단한 에코 서버와, 트램폴린을 사용해 이를 실행하는 코드입니다(연결이 끊기면 예를 들어
ConnectionLost를 발생시키는nonblocking_read,nonblocking_write및 그 밖의 I/O 코루틴이 존재한다고 가정합니다).:# coroutine function that echos data back on a connected # socket # def echo_handler(sock): while True: try: data = yield nonblocking_read(sock) yield nonblocking_write(sock, data) except ConnectionLost: pass # exit normally if connection lost # coroutine function that listens for connections on a # socket, and then launches a service "handler" coroutine # to service the connection # def listen_on(trampoline, sock, handler): while True: # get the next incoming connection connected_socket = yield nonblocking_accept(sock) # start another coroutine to handle the connection trampoline.add( handler(connected_socket) ) # Create a scheduler to manage all our coroutines t = Trampoline() # Create a coroutine instance to run the echo_handler on # incoming connections # server = listen_on( t, listening_socket("localhost","echo"), echo_handler ) # Add the coroutine to the scheduler t.add(server) # loop forever, accepting connections and servicing them # "in parallel" # t.run()
참조 구현
이 PEP에 설명된 모든 기능을 구현한 프로토타입 패치는 SourceForge 패치 #1223381(https://bugs.python.org/issue1223381)로 제공됩니다.
이 패치는 2005년 8월 1일~2일에 CVS에 커밋되었습니다.
감사의 말
Raymond Hettinger(PEP 288)와 Samuele Pedroni(PEP 325)는 제너레이터에 값이나 예외를 전달하는 아이디어와, 제너레이터를 닫는 기능을 처음으로 공식 제안했습니다. Timothy Delaney는 이 PEP의 제목을 제안했으며, Steven Bethard는 이전 버전의 편집을 도왔습니다. 또한 PEP 340의 감사의 글 절도 참조하십시오.
참고 문헌
TBD.
Copyright
This document has been placed in the public domain.