Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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는 이터레이터 프로토콜의 확장에 기반하여 코루틴에 대한 제한적인 접근 방식을 제안합니다. 현재 이터레이터는 값 생성을 마쳤음을 나타내기 위해 StopIteration 예외를 발생시킬 수 있습니다. 이 제안은 이 프로토콜에 또 다른 예외인 SuspendIteration을 추가합니다. 이 예외는 해당 이터레이터가 생성할 값이 더 있을 수 있지만 지금은 생성할 수 없음을 나타냅니다.

근거

현재 Python에 코루틴을 도입하는 접근 방식은 두 가지가 있습니다. Christian Tismer의 Stackless [6]는 ‘C’ 스택을 해킹하여 Python의 실행 모델을 근본부터 재구성합니다. 이 접근 방식은 동작하기는 하지만, 그 작동 방식을 설명하기 어렵고 이식성을 유지하기도 어렵습니다. 관련된 또 다른 접근 방식은 코루틴을 갖춘 레지스터 기반 가상 머신인 Parrot [7]으로 Python 코드를 컴파일하는 것입니다. 안타깝게도 이 두 해결책 모두 IronPython(CLR)이나 Jython(JavaVM)으로 이식할 수 없습니다.

이터레이터에 기반한 보다 제한적인 접근 방식이라면 애플리케이션 프로그래머에게 코루틴 기능을 제공하면서도 여러 런타임 간에 이식성을 유지할 수 있을 것으로 생각됩니다.

  • 이터레이터는 “C” 스택에 있지 않은 지역 변수에 상태를 보관합니다. 이터레이터는 next() 메서드 호출 사이에 유지되는 멤버 변수에 상태를 저장하는 클래스로 볼 수 있습니다.
  • 잡히지 않은 예외는 함수의 실행을 종료시킬 수 있지만, 잡히지 않은 예외가 이터레이터를 반드시 무효화할 필요는 없습니다. 제안된 예외인 SuspendIteration은 이 특성을 이용합니다. 다시 말해, next()를 한 번 호출했을 때 예외가 발생했다고 해서 반드시 이터레이터 자체가 더 이상 값을 생성할 수 없다는 뜻은 아닙니다.

이 새로운 예외가 영향을 미치는 곳은 네 군데입니다:

  • PEP 255의 단순 제너레이터 메커니즘을 확장하여, 이 SuspendIteration 예외를 안전하게 ‘잡고’ 현재 상태를 보관해 둔 뒤 예외를 호출자에게 전달하도록 할 수 있습니다.
  • itertools.izip 같은 표준 라이브러리의 다양한 이터레이터 필터 [9]가 이 예외를 인식하도록 하여 SuspendIteration을 투명하게 전파할 수 있게 해야 합니다.
  • 파일이나 소켓 리더처럼 I/O 연산으로부터 생성되는 이터레이터는 논블로킹 방식을 갖추도록 수정할 수 있습니다. 이 방식에서는 요청한 연산이 블록될 경우 SuspendIteration의 서브클래스를 발생시킵니다.
  • asyncore 라이브러리를 갱신하여 이터레이터에서 값을 가져오는 기본 ‘러너’를 제공하도록 할 수 있습니다. 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 콘텐츠를 yield하는 웹 페이지 제너레이터를 들 수 있습니다. 이것은 ‘생산자’의 예도 ‘소비자’의 예도 아니며, 오히려 필터의 예라는 점에 유의하십시오.

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

복잡하게 만드는 요인

위 예제는 간단하지만, 중간 제너레이터가 값을 ‘응축’하는 경우, 즉 값 하나를 생성할 때마다 두 개 이상의 값을 가져오는 경우에는 상황이 조금 더 복잡해집니다. 예를 들면 다음과 같습니다.

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 문제보다 나빠지지 않게 하고, 어쩌면 yield/finally 문제 자체를 없앨 수도 있을 것입니다. 이 부분은 더 많은 조사가 필요합니다.

API와 제약 사항

이 제안은 이터레이터 체인을 ‘일시 중단’하는 것만 다루며, (당연히) 일반 함수, 메서드 또는 “C” 확장 함수를 일시 중단하는 것은 다루지 않습니다. “C” 코드에서 제너레이터를 생성하는 것은 직접 지원되지 않을 수 있지만, SuspendIterator 의미 체계를 따르는 네이티브 “C” 이터레이터는 충분히 가능합니다.

저수준 구현

이 PEP의 저자는 아직 Python 실행 모델을 잘 알지 못하여 이 부분에 대해 의견을 제시할 수 없습니다.

참고 문헌