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

Python 개선 제안 한국어 번역

PEP 255 – 단순 제너레이터

Author:
Neil Schemenauer <nas at arctrix.com>, Tim Peters <tim.peters at gmail.com>, Magnus Lie Hetland <magnus at hetland.org>
Status:
Final
Type:
Standards Track
Requires:
234
Created:
18-May-2001
Python-Version:
2.2
Post-History:
14-Jun-2001, 23-Jun-2001

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

개요

이 PEP는 Python에 제너레이터 개념과 제너레이터와 함께 사용하는 새로운 문인 yield 문을 도입합니다.

동기

생성기 함수가 생성한 값 사이의 상태를 유지해야 할 만큼 어려운 작업을 수행하는 경우, 대부분의 프로그래밍 언어는 생성된 각 값과 함께 호출할 콜백 함수를 생성기의 인자 목록에 추가하는 것 외에는 쾌적하고 효율적인 해결책을 제공하지 않습니다.

예를 들어, 표준 라이브러리의 tokenize.py는 이 접근 방식을 사용합니다. 호출자는 tokenize()가 다음 토큰을 찾을 때마다 호출되는 tokeneater 함수를 tokenize()에 전달해야 합니다. 이를 통해 tokenize를 자연스러운 방식으로 작성할 수 있지만, tokenize를 호출하는 프로그램은 일반적으로 콜백 사이에 마지막으로 어떤 토큰을 보았는지 기억해야 하므로 복잡해집니다. tabnanny.pytokeneater 함수가 이를 잘 보여주는 예로, 전역 변수에 상태 머신을 유지하여 콜백 사이에 이미 본 것과 다음에 보기를 기대하는 것을 기억합니다. 이를 올바르게 작동하도록 만드는 일은 어려웠으며, 여전히 사람들이 이해하기 어렵습니다. 안타깝게도 이것은 이러한 접근 방식에서 전형적인 일입니다.

또 다른 방법은 tokenize가 Python 프로그램 전체를 한 번에 큰 리스트로 구문 분석하도록 하는 것이었을 수 있습니다. 그러면 tokenize 클라이언트는 지역 변수와 지역 제어 흐름(예: 루프와 중첩된 if 문)을 사용하여 상태를 추적하는 자연스러운 방식으로 작성할 수 있습니다. 그러나 이는 실용적이지 않습니다. 프로그램이 매우 클 수 있으므로 전체 구문 분석 결과를 구체화하는 데 필요한 메모리에 사전적으로 정해진 상한을 둘 수 없습니다. 또한 일부 tokenize 클라이언트는 프로그램 앞부분에 특정 항목(예: future 문 또는 IDLE에서 수행하는 것처럼 첫 번째 들여쓰기 문)만 나타나는지 확인하려고 하므로, 먼저 프로그램 전체를 구문 분석하는 것은 시간을 심각하게 낭비합니다.

또 다른 방법은 tokenize를 iterator로 만들어 .next() 메서드가 호출될 때마다 다음 토큰을 전달하도록 하는 것입니다. 이는 많은 결과를 담은 리스트를 사용하는 것과 같은 방식으로 호출자에게 편리하면서도, 메모리 문제와 “일찍 빠져나가고 싶다면 어떻게 하지?”라는 단점이 없습니다. 그러나 이 경우 tokenize가 .next() 호출 사이에 자신의 상태를 기억해야 하는 부담을 지게 되며, 독자는 tokenize.tokenize_loop()를 잠깐 보기만 해도 이것이 얼마나 끔찍한 작업인지 깨닫게 됩니다. 또는 일반적인 트리 구조의 노드를 생성하는 재귀 알고리즘을 생각해 보십시오. 이를 이터레이터 프레임워크로 변환하려면 재귀를 수동으로 제거하고 순회 상태를 직접 유지해야 합니다.

네 번째 방법은 생성기와 소비자를 별도의 스레드에서 실행하는 것입니다. 이렇게 하면 양쪽 모두 자연스러운 방식으로 상태를 유지할 수 있으므로 둘 다 편리합니다. 실제로 Python 소스 배포판의 Demo/threads/Generator.py는 이를 일반적인 방식으로 수행할 수 있는 유용한 동기화 통신 제너레이터 클래스를 제공합니다. 다만 스레드가 없는 플랫폼에서는 작동하지 않으며, 스레드를 지원하는 플랫폼에서도 스레드 없이 달성할 수 있는 성능에 비해 매우 느립니다.

마지막 방법은 경량 코루틴을 지원하는 Stackless [1] (PEP 219) 변형 Python 구현을 대신 사용하는 것입니다. 이는 스레드 방식과 프로그래밍 측면에서 거의 동일한 이점을 제공하지만 훨씬 효율적입니다. 그러나 Stackless는 논란의 여지가 있는 Python 핵심의 재구성이며, Jython에서 동일한 의미론을 구현하지 못할 수도 있습니다. 이 PEP는 이를 논의할 곳이 아니므로, 여기서는 제너레이터가 현재 CPython 구현에 쉽게 맞는 방식으로 Stackless 기능의 유용한 일부를 제공하며, 다른 Python 구현에서도 비교적 간단하게 구현할 수 있다고 여겨진다는 점만 말해 두겠습니다.

이것으로 현재의 대안은 모두 소진됩니다. 다른 고급 언어들도 쾌적한 해결책을 제공하는데, 특히 CLU의 이터레이터에서 영감을 받은 Sather의 이터레이터 [2]와 모든 표현식이 제너레이터입니다라는 독특한 언어인 Icon의 제너레이터 [3]가 그러합니다. 이들 사이에는 차이점이 있지만 기본 아이디어는 동일합니다. 중간 결과(“다음 값”)를 호출자에게 반환할 수 있으면서도 함수의 지역 상태를 유지하여, 함수가 중단한 바로 그 지점에서 다시 재개될 수 있는 일종의 함수를 제공하는 것입니다. 아주 간단한 예를 들어 보겠습니다.:

def fib():
    a, b = 0, 1
    while 1:
       yield b
       a, b = b, a+b

fib()가 처음 호출되면 a를 0으로, b를 1로 설정한 다음 b를 호출자에게 산출합니다. 호출자는 1을 확인합니다. fib가 재개되면, 그 관점에서 yield 문은 말하자면 print 문과 실제로 같습니다. fib는 모든 로컬 상태를 그대로 유지한 채 yield 다음부터 계속 실행합니다. ab는 이어서 각각 1과 1이 되고, fibyield로 돌아가 호출자에게 1을 산출합니다. 그리고 계속됩니다. fib의 관점에서는 콜백을 통해 전달하는 것처럼 결과의 시퀀스를 전달할 뿐입니다. 그러나 호출자의 관점에서 fib 호출은 원할 때 재개할 수 있는 이터러블 객체입니다. 스레드 방식과 마찬가지로 이를 통해 양쪽을 가장 자연스러운 방식으로 코딩할 수 있지만, 스레드 방식과 달리 효율적으로 모든 플랫폼에서 수행할 수 있습니다. 실제로 제너레이터를 재개하는 데 드는 비용은 함수 호출보다 크지 않아야 합니다.

동일한 종류의 접근 방식은 많은 생산자/소비자 함수에 적용됩니다. 예를 들어 tokenize.py는 콜백 함수를 인자로 호출하는 대신 다음 토큰을 산출할 수 있고, tokenize 클라이언트는 자연스러운 방식으로 토큰을 순회할 수 있습니다. 파이썬 제너레이터는 파이썬 iterator의 한 종류이지만, 특히 강력한 종류입니다.

사양: Yield

새로운 문이 도입됩니다.:

yield_stmt:    "yield" expression_list

yield는 새로운 키워드이므로, 이를 단계적으로 도입하려면 future 문(PEP 236)이 필요합니다. 최초 릴리스에서는 제너레이터를 사용하려는 모듈이 다음 줄을 포함해야 합니다.:

from __future__ import generators

해당 줄을 파일 상단 근처에 두어야 합니다(자세한 내용은 PEP 236 참조). future 문 없이 식별자 yield를 사용하는 모듈은 경고를 발생시킵니다. 다음 릴리스에서는 yield가 언어 키워드가 되므로 future 문은 더 이상 필요하지 않습니다.

yield문은 함수 내부에서만 사용할 수 있습니다. yield문을 포함하는 함수를 제너레이터 함수라고 합니다. 제너레이터 함수는 모든 면에서 일반 함수 객체이지만, 코드 객체의 co_flags 멤버에 새로운 CO_GENERATOR 플래그가 설정되어 있습니다.

제너레이터 함수를 호출하면 실제 인자는 일반적인 방식으로 함수 지역 형식 인자 이름에 바인딩되지만, 함수 본문의 코드는 실행되지 않습니다. 대신 제너레이터-이터레이터 객체가 반환됩니다. 이 객체는 iterator protocol을 따르므로, 특히 for 루프에서 자연스러운 방식으로 사용할 수 있습니다. 문맥상 의도가 분명한 경우에는 한정되지 않은 이름인 “제너레이터”를 제너레이터 함수 또는 제너레이터-이터레이터 중 하나를 가리키는 데 사용할 수 있다는 점에 유의하십시오.

제너레이터-이터레이터의 .next()메서드가 호출될 때마다 제너레이터 함수 본문의 코드는 yield문이나 return문(아래 참조)을 만날 때까지 또는 본문의 끝에 도달할 때까지 실행됩니다.

yield문을 만나면 함수의 상태가 고정되고, expression_list의 값이 .next()의 호출자에게 반환됩니다. 여기서 “고정”이란 현재 지역 변수 바인딩, 명령 포인터, 내부 평가 스택을 비롯한 모든 지역 상태가 유지된다는 뜻입니다. 따라서 다음에 .next()가 호출되면 함수가 yield문을 또 다른 외부 호출인 것처럼 정확히 계속 실행할 수 있을 만큼 충분한 정보가 저장됩니다.

제한: yield문은 try/finally구성의 try절에서 허용되지 않습니다. 문제는 제너레이터가 다시 시작된다는 보장이 없으므로 finally 블록이 실행된다는 보장도 없다는 점입니다. 이는 finally의 목적을 지나치게 훼손하므로 감수할 수 없습니다.

제한: 제너레이터는 실행 중인 동안 재개할 수 없습니다.:

>>> def g():
...     i = me.next()
...     yield i
>>> me = g()
>>> me.next()
Traceback (most recent call last):
 ...
 File "<string>", line 2, in g
ValueError: generator already executing

사양: Return

제너레이터 함수에는 다음 형식의 return 문도 포함할 수 있습니다.:

return

제너레이터 본문의 return 문에는 expression_list를 사용할 수 없다는 점에 유의하십시오(물론 제너레이터 내부에 중첩된 제너레이터가 아닌 함수의 본문에는 사용할 수 있습니다).

return 문을 만나면 제어는 모든 함수의 return과 마찬가지로 진행되며, 해당하는 finally 절(있는 경우)을 실행합니다. 그런 다음 이터레이터가 소진되었음을 알리는 StopIteration 예외가 발생합니다. 명시적인 return 없이 제너레이터의 끝까지 제어가 흐르는 경우에도 StopIteration 예외가 발생합니다.

제너레이터 함수와 비제너레이터 함수 모두에서 return은 “완료했으며, 반환할 만한 흥미로운 것이 없습니다”라는 의미임을 유의하십시오.

return이 항상 StopIteration을 발생시키는 것과 동등한 것은 아니라는 점에 유의하십시오. 차이는 둘러싸고 있는 try/except 구문이 처리되는 방식에 있습니다. 예를 들어,:

>>> def f1():
...     try:
...         return
...     except:
...        yield 1
>>> print list(f1())
[]

모든 함수에서와 마찬가지로 return은 단순히 빠져나가기 때문이지만,:

>>> def f2():
...     try:
...         raise StopIteration
...     except:
...         yield 42
>>> print list(f2())
[42]

StopIteration은 다른 모든 예외와 마찬가지로 단순한 except에 의해 포착되기 때문입니다.

사양: 제너레이터와 예외 전파

처리되지 않은 예외(StopIteration을 포함하지만 이에 국한되지 않음)가 제너레이터 함수에 의해 발생하거나 제너레이터 함수를 통과하면, 해당 예외는 일반적인 방식으로 호출자에게 전달되며, 이후 제너레이터 함수를 재개하려는 시도에서는 StopIteration이 발생합니다. 다시 말해, 처리되지 않은 예외는 제너레이터의 유용한 수명을 종료합니다.

예제(관용적인 방식은 아니지만 요점을 설명하기 위한 것입니다):

>>> def f():
...     return 1/0
>>> def g():
...     yield f()  # the zero division exception propagates
...     yield 42   # and we'll never get here
>>> k = g()
>>> k.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 2, in g
  File "<stdin>", line 2, in f
ZeroDivisionError: integer division or modulo by zero
>>> k.next()  # and the generator cannot be resumed
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration
>>>

사양: Try/Except/Finally

앞서 언급했듯이 try/finally 구문의 try 절에서는 yield를 사용할 수 없습니다. 따라서 제너레이터는 중요한 리소스를 매우 신중하게 할당해야 합니다. 그 밖에 finally 절, except 절 또는 try/except 구문의 try 절에 yield가 나타나는 것에는 제한이 없습니다.:

>>> def f():
...     try:
...         yield 1
...         try:
...             yield 2
...             1/0
...             yield 3  # never get here
...         except ZeroDivisionError:
...             yield 4
...             yield 5
...             raise
...         except:
...             yield 6
...         yield 7     # the "raise" above stops this
...     except:
...         yield 8
...     yield 9
...     try:
...         x = 12
...     finally:
...        yield 10
...     yield 11
>>> print list(f())
[1, 2, 4, 5, 8, 9, 10, 11]
>>>

예제

# A binary tree class.
class Tree:

    def __init__(self, label, left=None, right=None):
        self.label = label
        self.left = left
        self.right = right

    def __repr__(self, level=0, indent="    "):
        s = level*indent + `self.label`
        if self.left:
            s = s + "\n" + self.left.__repr__(level+1, indent)
        if self.right:
            s = s + "\n" + self.right.__repr__(level+1, indent)
        return s

    def __iter__(self):
        return inorder(self)

# Create a Tree from a list.
def tree(list):
    n = len(list)
    if n == 0:
        return []
    i = n / 2
    return Tree(list[i], tree(list[:i]), tree(list[i+1:]))

# A recursive generator that generates Tree labels in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

# Show it off: create a tree.
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# Print the nodes of the tree in in-order.
for x in t:
    print x,
print

# A non-recursive generator.
def inorder(node):
    stack = []
    while node:
        while node.left:
            stack.append(node)
            node = node.left
        yield node.label
        while not node.right:
            try:
                node = stack.pop()
            except IndexError:
                return
            yield node.label
        node = node.right

# Exercise the non-recursive generator.
for x in t:
    print x,
print

두 출력 블록은 다음을 표시합니다.:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

질문과 답변

def를 재사용하지 않고 새 키워드를 사용하면 안 됩니까?

아래의 BDFL 선언 섹션을 참조하십시오.

yield에 새 키워드를 사용하는 이유는 무엇입니까? 대신 내장 함수를 사용하면 안 됩니까?

Python에서는 제어 흐름을 키워드를 통해 표현하는 것이 훨씬 더 적절하며, yield는 제어 구문입니다. 또한 Jython에서 효율적으로 구현하려면 컴파일러가 컴파일 시점에 잠재적인 일시 중지 지점을 판별할 수 있어야 하며, 새 키워드를 사용하면 이를 쉽게 할 수 있다고 여겨집니다. CPython 참조 구현도 어떤 함수가 제너레이터 함수인지 감지하기 위해 이를 적극적으로 활용합니다(물론 def를 대신하는 새 키워드를 사용하면 CPython에서는 이 문제가 해결되지만, “왜 새 키워드인가?”라고 묻는 사람들은 어떤 새 키워드도 원하지 않습니다).

그렇다면 새 키워드 없이 다른 특수 구문을 사용하면 안 됩니까?

예를 들어 yield 3 대신 다음 중 하나를 사용하는 것입니다.:

return 3 and continue
return and continue 3
return generating 3
continue return 3
return >> , 3
from generator return 3
return >> 3
return << 3
>> 3
<< 3
* 3

제가 하나를 빠뜨렸습니까(<wink>)? 수백 개의 메시지 중 이러한 대안을 제안한 것은 세 개였으며, 그 메시지들에서 위의 내용을 추려 냈습니다. 새 키워드가 필요하지 않다면 좋겠지만, yield를 매우 명확하게 만드는 편이 더 좋습니다. 이전에는 의미가 없던 키워드나 연산자의 나열을 이해해야 yield가 발생한다는 것을 추론할 필요가 없었으면 합니다. 그래도 이것이 충분한 관심을 끌면, 제안자들은 하나의 합의된 제안으로 의견을 모아야 하며, Guido가 이에 대해 선언을 내릴 것입니다.

return을 아예 허용합니까? 종료를 raise StopIteration으로 표기하도록 강제하지 않는 이유는 무엇입니까?

StopIteration의 작동 방식은 Python 2.1에서 IndexError의 작동 방식과 마찬가지로 저수준 세부 사항입니다. 구현은 내부적으로 잘 정의된 something을 수행해야 하며, Python은 고급 사용자를 위해 이러한 메커니즘을 노출합니다. 그렇다고 해서 모든 사용자가 그 수준에서 작업하도록 강제해야 한다는 주장은 아닙니다. return은 어떤 종류의 함수에서든 “이제 끝났습니다”를 의미하며, 설명하고 사용하기 쉽습니다. return이 try/except 구문에서 항상 raise StopIteration과 동등한 것은 아니라는 점에도 유의하십시오(“Specification: Return” 절을 참조하십시오).

그렇다면 return에서도 표현식을 허용하지 않는 이유는 무엇입니까?

언젠가는 그렇게 할 수도 있습니다. Icon에서 return expr은 “이제 끝났습니다”와 “하지만 마지막으로 유용한 값을 하나 더 반환하고 싶으며, 이것이 바로 그 값입니다”를 모두 의미합니다. 처음에는, 그리고 return expr을 사용할 설득력 있는 용도가 없는 상황에서는 값을 전달하는 데 yield만 독점적으로 사용하는 편이 더 깔끔합니다.

BDFL 선언

쟁점

제너레이터 함수와 비제너레이터 함수를 구별하기 위해 def 대신 또 다른 새 키워드(예를 들어 gen 또는 generator)를 도입하거나, 그 밖의 방식으로 구문을 변경합니다.

반대 의견

실제로(제너레이터를 어떻게 생각하든) 제너레이터는 함수이지만, 다시 시작할 수 있다는 특징이 있습니다. 제너레이터를 설정하는 방식은 비교적 사소한 기술적 문제이며, 새 키워드를 도입하면 제너레이터가 시작되는 방식(제너레이터 수명에서 중요하지만 짧은 부분)의 작동 원리를 부적절하게 과도하게 강조하게 됩니다.

찬성 의견

실제로(이를 어떻게 생각하는지에 따르면) 제너레이터 함수는 마치 마법처럼 제너레이터 이터레이터를 생성하는 팩토리 함수입니다. 이런 점에서 제너레이터 함수는 비제너레이터 함수와 근본적으로 다르며, 함수라기보다는 생성자처럼 동작하므로 def를 재사용하는 것은 기껏해야 혼란스럽습니다. 본문 속에 묻혀 있는 yield문만으로는 의미론이 이처럼 다르다는 사실을 충분히 경고할 수 없습니다.

BDFL

def를 그대로 유지합니다. 어느 쪽의 주장도 완전히 설득력 있지는 않으므로, 언어 설계자로서의 직관에 의지해 보았습니다. 그 직관은 PEP에서 제안한 구문이 정확히 옳다고 말합니다. 너무 뜨겁지도, 너무 차갑지도 않습니다. 하지만 그리스 신화의 델포이 신탁처럼, 그 직관은 그 이유를 말해 주지 않으므로 PEP 구문에 반대하는 주장에 대한 반박도 제시할 수 없습니다. (이미 제시된 반박에 동의하는 것 외에) 제가 생각해 낼 수 있는 최선의 답은 “FUD”입니다. 이것이 첫날부터 언어의 일부였다면 Andrew Kuchling의 “Python Warts” 페이지에 실렸을 가능성은 매우 낮다고 생각합니다.

참조 구현

현재 구현은 예비 상태(문서는 없지만 충분히 테스트되었고 견고한 상태)이며, Python의 CVS 개발 트리 [5]의 일부입니다. 이를 사용하려면 소스에서 Python을 빌드해야 합니다.

이는 Neil Schemenauer가 작성한 이전 패치 [4]에서 파생되었습니다.

각주 및 참고 자료