PEP 334 – SuspendIteration을 통한 간단한 코루틴
- Author:
- Clark C. Evans <cce at clarkevans.com>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 26-Aug-2004
- Python-Version:
- 3.0
- Post-History:
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Twisted [1] 및 Peak [2]는 이벤트 큐 또는 지연 실행을 통한 협력적 멀티태스킹을 기반으로 합니다. 애플리케이션 개발에 대한 이 접근 방식은 스레드를 사용하지 않으므로 여러 문제 [3]를 피할 수 있지만, 다른 종류의 프로그래밍 과제를 만듭니다. I/O 작업이 블로킹될 경우, 다른 요청이 진행될 수 있도록 사용자 요청을 일시 중단해야 합니다. 코루틴 [4]이라는 개념은 애플리케이션 개발자가 이러한 상태 관리의 어려움에 대처하는 데 도움을 줄 수 있습니다.
이 PEP는 iterator protocol에 대한 확장을 기반으로 코루틴에 제한적으로 접근하는 방법을 제안합니다. 현재 이터레이터는 값 생성을 완료했음을 나타내기 위해 StopIteration 예외를 발생시킬 수 있습니다. 이 제안은 이 프로토콜에 SuspendIteration이라는 또 다른 예외를 추가합니다. 이 예외는 해당 이터레이터가 생성할 값이 더 있을 수 있지만 현재는 생성할 수 없음을 나타냅니다.
근거
현재 Python에 코루틴을 도입하는 방법에는 두 가지가 있습니다. Christian Tismer의 Stackless [6]는 ‘C’ 스택을 수정하여 Python의 실행 모델을 처음부터 재구성합니다. 이 접근 방식은 작동하지만, 그 동작을 설명하고 이식 가능하게 유지하기가 어렵습니다. 이와 관련된 접근 방식은 코루틴을 지원하는 레지스터 기반 가상 머신인 Parrot [7]용으로 Python 코드를 컴파일하는 것입니다. 안타깝게도 이 두 해결책 모두 IronPython (CLR) 또는 Jython (JavaVM)과는 호환되지 않습니다.
이터레이터를 기반으로 하는 보다 제한적인 접근 방식이라면 애플리케이션 프로그래머에게 코루틴 기능을 제공하면서도 런타임 전반에서 이식 가능할 것으로 여겨집니다.
- 이터레이터는 “C” 스택에 저장되지 않는 지역 변수에 상태를 유지합니다. 이터레이터는 클래스처럼 볼 수 있으며, 상태는 next() 메서드 호출 전반에 걸쳐 지속되는 멤버 변수에 저장됩니다.
- 처리되지 않은 예외가 함수의 실행을 종료할 수는 있지만, 처리되지 않은 예외가 이터레이터를 반드시 무효화하는 것은 아닙니다. 제안된 예외인 SuspendIteration은 이 기능을 사용합니다. 다시 말해, next() 호출 한 번의 결과로 예외가 발생했다고 해서 이터레이터 자체가 더 이상 값을 생성할 수 없다는 의미가 반드시 되는 것은 아닙니다.
이 새로운 예외가 영향을 미치는 곳은 네 가지입니다.
- PEP PEP 255의 단순 제너레이터 메커니즘은 이 SuspendIteration 예외를 안전하게 ‘catch’하여 현재 상태를 저장하고 호출자에게 예외를 전달하도록 확장할 수 있습니다.
- itertools.izip과 같은 표준 라이브러리의 다양한 이터레이터 필터 [9]는 이 예외를 인식하여 SuspendIteration을 투명하게 전파할 수 있도록 만들어야 합니다.
- 파일 또는 소켓 리더와 같은 I/O 작업에서 생성되는 이터레이터를 논블로킹 방식으로 수정할 수 있습니다. 요청된 작업이 블로킹될 경우 이 옵션은 SuspendIteration의 서브클래스를 발생시킵니다.
- asyncore 라이브러리를 업데이트하여 이터레이터에서 값을 가져오는 기본 ‘runner’를 제공할 수 있습니다. SuspendIteration 예외가 포착되면 실행 목록 [10]의 다음 이터레이터로 넘어갑니다. Twisted와 같은 외부 프레임워크는 FreeBSD의 kqueue 또는 Linux의 epoll을 기반으로 하는 대체 구현을 제공할 수 있습니다.
이러한 변경 사항이 극적으로 보일 수 있지만, 연속체가 제공하는 유용성에 비하면 필요한 작업량은 매우 적습니다.
의미론
이 절에서는 이 새로운 SuspendIteration 예외의 도입이 높은 수준에서 어떻게 동작하는지 설명합니다.
단순 이터레이터
현재 이터레이터의 기능은 ‘one’과 ‘two’라는 두 값을 생성하는 간단한 예제에서 가장 잘 확인할 수 있습니다.
class States:
def __iter__(self):
self._next = self.state_one
return self
def next(self):
return self._next()
def state_one(self):
self._next = self.state_two
return "one"
def state_two(self):
self._next = self.state_stop
return "two"
def state_stop(self):
raise StopIteration
print list(States())
물론 다음 제너레이터를 사용하여 동등한 반복을 만들 수도 있습니다.:
def States():
yield 'one'
yield 'two'
print list(States())
SuspendIteration 소개
위의 제너레이터가 ‘one’과 ‘two’를 생성하는 사이에 소켓 읽기에서 블록될 수 있다고 가정하십시오. 이 경우 이터레이터가 아직 생성을 완료하지 않았지만 현재 순간에는 값을 제공할 수 없음을 알리기 위해 SuspendIteration을 발생시키고자 할 것입니다.
from random import randint
from time import sleep
class SuspendIteration(Exception):
pass
class NonBlockingResource:
"""Randomly unable to produce the second value"""
def __iter__(self):
self._next = self.state_one
return self
def next(self):
return self._next()
def state_one(self):
self._next = self.state_suspend
return "one"
def state_suspend(self):
rand = randint(1,10)
if 2 == rand:
self._next = self.state_two
return self.state_two()
raise SuspendIteration()
def state_two(self):
self._next = self.state_stop
return "two"
def state_stop(self):
raise StopIteration
def sleeplist(iterator, timeout = .1):
"""
Do other things (e.g. sleep) while resource is
unable to provide the next value
"""
it = iter(iterator)
retval = []
while True:
try:
retval.append(it.next())
except SuspendIteration:
sleep(timeout)
continue
except StopIteration:
break
return retval
print sleeplist(NonBlockingResource())
실제 상황에서는 NonBlockingResource가 파일 이터레이터, 소켓 핸들 또는 기타 I/O 기반 생성자일 것입니다. sleeplist는 대신 asyncore나 Twisted에서 볼 수 있는 것과 같은 비동기 리액터일 것입니다. 물론 비블로킹 리소스는 제너레이터로 작성할 수도 있습니다.:
def NonBlockingResource():
yield "one"
while True:
rand = randint(1,10)
if 2 == rand:
break
raise SuspendIteration()
yield "two"
대부분의 실제 콘텐츠 제너레이터는 애플리케이션 코드에 있지 않고 저수준 I/O 기반 작업에 있으므로 ‘suspend’ 키워드를 추가할 필요는 없습니다. 대부분의 프로그래머는 SuspendIteration() 메커니즘에 노출될 필요가 없으므로 키워드는 필요하지 않습니다.
애플리케이션 이터레이터
앞의 예제는 다소 작위적이며, 더 ‘현실적인’ 예제로는 HTML 콘텐츠를 생성하고 데이터베이스에서 가져오는 웹 페이지 제너레이터가 있습니다. 이것은 ‘producer’의 예도 ‘consumer’의 예도 아니며, 오히려 필터의 예라는 점에 유의하십시오.
def ListAlbums(cursor):
cursor.execute("SELECT title, artist FROM album")
yield '<html><body><table><tr><td>Title</td><td>Artist</td></tr>'
for (title, artist) in cursor:
yield '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
yield '</table></body></html>'
물론 문제는 어떤 행이 반환되기 전에 데이터베이스가 한동안 블록될 수 있고, 실행 중에 행이 한 번에 10개 또는 100개씩 묶음으로 반환될 수 있다는 점입니다. 이상적으로는 데이터베이스가 다음 행 집합을 기다리며 블록될 경우 다른 사용자 연결을 처리할 수 있어야 합니다. 위 코드에는 SuspendIterator가 전혀 없다는 점에 유의하십시오. 제대로 구현된다면 애플리케이션 개발자는 동시성 문제보다 기능에 집중할 수 있을 것입니다.
위 제너레이터가 생성한 이터레이터는 상태를 유지하는 데 필요한 마법을 수행하면서도 예외를 하위 수준의 비동기 프레임워크로 전달해야 합니다. 다음은 해당 이터레이터를 클래스로 작성했을 때의 모습에 대한 예제입니다.:
class ListAlbums:
def __init__(self, cursor):
self.cursor = cursor
def __iter__(self):
self.cursor.execute("SELECT title, artist FROM album")
self._iter = iter(self._cursor)
self._next = self.state_head
return self
def next(self):
return self._next()
def state_head(self):
self._next = self.state_cursor
return "<html><body><table><tr><td>\
Title</td><td>Artist</td></tr>"
def state_tail(self):
self._next = self.state_stop
return "</table></body></html>"
def state_cursor(self):
try:
(title,artist) = self._iter.next()
return '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
except StopIteration:
self._next = self.state_tail
return self.next()
except SuspendIteration:
# just pass-through
raise
def state_stop(self):
raise StopIteration
복잡하게 만드는 요인
위 예제는 간단하지만, 중간 제너레이터가 값을 ‘condense’하는 경우, 즉 생성하는 각 값에 대해 두 개 이상의 값을 가져오는 경우에는 상황이 조금 더 복잡합니다. 예를 들어 다음과 같습니다.
def pair(iterLeft,iterRight):
rhs = iter(iterRight)
lhs = iter(iterLeft)
while True:
yield (rhs.next(), lhs.next())
이 경우 오른쪽 이터레이터나 왼쪽 이터레이터 중 어느 하나가 SuspendIteration을 발생시키는 상황을 처리하려면 해당 이터레이터의 동작이 조금 더 정교해야 합니다. 생성 컨텍스트에서 발생하는 SuspendIterator 예외가 일어날 수 있는 중간 상태를 인식하도록 제너레이터를 분해하는 문제인 듯합니다.
class pair:
def __init__(self, iterLeft, iterRight):
self.iterLeft = iterLeft
self.iterRight = iterRight
def __iter__(self):
self.rhs = iter(iterRight)
self.lhs = iter(iterLeft)
self._temp_rhs = None
self._temp_lhs = None
self._next = self.state_rhs
return self
def next(self):
return self._next()
def state_rhs(self):
self._temp_rhs = self.rhs.next()
self._next = self.state_lhs
return self.next()
def state_lhs(self):
self._temp_lhs = self.lhs.next()
self._next = self.state_pair
return self.next()
def state_pair(self):
self._next = self.state_rhs
return (self._temp_rhs, self._temp_lhs)
이 제안은 이 클래스 기반 방식으로 작성한 해당 이터레이터를 기존 제너레이터에 대해 구현할 수 있다고 가정합니다. 과제는 중단이 발생할 수 있는 제너레이터 내부의 서로 다른 상태를 식별하는 것인 듯합니다.
리소스 정리
현재 제너레이터 메커니즘은 예외와 특이하게 상호 작용하여 try/finally 블록 안에서는 ‘yield’ 문을 사용할 수 없습니다. SuspendIterator 예외도 이와 유사한 또 다른 문제를 야기합니다. 이 문제의 영향은 명확하지 않습니다. 그러나 앞 절에서 했던 것처럼 제너레이터를 상태 머신으로 다시 작성하면 이 문제를 해결할 수 있으며, 그 결과 yield/finally 상황이 더 나빠지지 않게 할 뿐 아니라 어쩌면 이 상황을 완전히 제거할 수도 있습니다. 이 영역에 대해서는 더 많은 조사가 필요합니다.
API 및 제한 사항
이 제안은 이터레이터 체인을 ‘중단’하는 것만 다루며, 일반 함수, 메서드 또는 “C” 확장 함수를 ‘중단’하는 것은 (물론) 다루지 않습니다. “C” 코드에서 제너레이터를 생성하는 직접적인 지원은 불가능할 수 있지만, SuspendIterator 의미 체계를 준수하는 네이티브 “C” 이터레이터는 분명히 가능합니다.
저수준 구현
PEP 작성자는 아직 Python 실행 모델에 익숙하지 않으므로 이 영역에 관해 논평할 수 없습니다.
참고 자료
Copyright
This document has been placed in the public domain.