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

Python 개선 제안 한국어 번역

PEP 323 – 복사 가능한 이터레이터

Author:
Alex Martelli <aleaxit at gmail.com>
Status:
Deferred
Type:
Standards Track
Created:
25-Oct-2003
Python-Version:
2.5
Post-History:
29-Oct-2003

Table of Contents

번역·라이선스 안내

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

보류

이 PEP는 보류되었습니다. 복사 가능한 이터레이터는 좋은 아이디어이지만, 4년이 지나도록 구현이 나오지 않았고 폭넓은 관심도 생기지 않았습니다.

초록

이 PEP는 일부 이터레이터 타입이 특정 요구 사항을 충족하는 __copy__ 메서드를 노출하여 인스턴스의 얕은 복사를 지원해야 한다고 제안하며, 이터레이터를 사용하는 코드가 그러한 __copy__ 메서드가 있을 때 이를 어떻게 활용할 수 있는지 보여 줍니다.

업데이트 및 의견

__copy__ 지원은 Py2.4의 itertools.tee()에 포함되었습니다.

기존 이터레이터에 __copy__ 메서드를 추가하면 tee()에서의 동작이 바뀝니다. 현재는 복사된 이터레이터가 원래 이터레이터에 묶인 채로 남아 있습니다. 원래 이터레이터가 진행하면 모든 복사본도 함께 진행합니다. 이상 현상이 생기지 않도록 원래 이터레이터를 덮어쓰는 것이 좋은 관행입니다: a,b=tee(a). 이 관행을 따르지 않는 코드는 이터레이터에 __copy__ 메서드가 추가되면 의미상의 변화를 겪을 수 있습니다.

동기

Python 2.3까지는 대부분의 내장 이터레이터 타입이 사용자가 그 인스턴스를 복사하는 것을 허용하지 않습니다. 클라이언트가 인스턴스에 대해 copy.copy를 호출할 수 있게 해 주는 사용자 작성 이터레이터는 복사의 결과로 원본과 독립적으로 이터레이션할 수 있는 별도의 이터레이터 객체를 반환할 수도 있고, 그렇지 않을 수도 있습니다.

현재 사용자 작성 이터레이터 타입에서 copy.copy에 대한 “지원”은 거의 예외 없이 “우연”입니다. 즉, Python 표준 라이브러리 copy 모듈에 있는 copy 메서드의 표준 메커니즘이 복사본을 만들어 반환하기는 합니다. 하지만 그 복사본이 원본과 독립적으로 순회 가능한 이터러블이 되는 것은, 해당 클래스의 인스턴스에서 .next()를 호출했을 때 일부 속성의 기존 값을 변경하는 방식이 아니라 오로지 일부 속성을 새 값에 다시 바인딩하는 방식으로만 인스턴스 상태가 바뀌는 경우에 한합니다.

예를 들어 “인덱스” 상태를 정수 속성으로 보관하는 이터레이터는 아마 쓸 만한 복사본을 제공할 것입니다. (정수는 불변이므로) .next()는 아마 그 속성을 다시 바인딩할 뿐이기 때문입니다. 반면에 “인덱스” 상태를 리스트 속성으로 보관하는 이터레이터는 .next()가 실행될 때 같은 리스트 객체를 변경할 가능성이 높으며, 따라서 그러한 이터레이터의 복사본은 원본과 분리되어 독립적으로 순회할 수 있는 이터러블이 되지 않습니다.

이러한 현재 상황을 고려하면 어떤 이터레이터 객체에 대한 copy.copy(it)는 그다지 유용하지 않으며, 따라서 널리 사용되지도 않습니다. 하지만 이터레이터의 “스냅숏”을 “책갈피”로 얻어 두고, 시퀀스를 따라 계속 이터레이션하다가 나중에 같은 시퀀스를 책갈피 지점부터 다시 이터레이션할 수 있으면 유용한 경우가 많습니다. 이러한 “책갈피”를 지원하기 위해 2.4에서 itertools 모듈에 ‘tee’ 함수가 추가되었으며, 다음과 같이 사용합니다:

it, bookmark = itertools.tee(it)

‘it’의 이전 값은 다시 사용하면 안 되며, 이 때문에 이 전형적인 사용 관용구에서는 이름을 다시 바인딩합니다. 이 호출 이후, ‘it’과 ‘bookmark’는 ‘it’의 원래 값과 같은 기반 시퀀스 위에서 각각 독립적으로 순회할 수 있는 이터러블 이터레이터가 됩니다. 이로써 “이터레이터 복사”에 대한 애플리케이션의 요구가 충족됩니다.

하지만 itertools.tee가 인자로 전달받은 이터레이터의 성질에 대해 아무런 가정도 할 수 없을 때는, ‘tee’된 두 이터레이터 중 하나는 지나갔지만 아직 둘 다 지나가지는 않은 모든 항목을 메모리에 저장해야 합니다. 두 이터레이터의 진행 정도가 서로 크게 벌어지면 메모리 측면에서 상당히 큰 비용이 들 수 있습니다. 실제로 어떤 경우에는 부분 시퀀스를 반복해서 훑을 수 있도록 이터레이터로부터 리스트를 만드는 편이 낫거나, 그것도 메모리 측면에서 비용이 너무 크다면 역시 반복해서 이터레이션할 수 있도록 항목을 디스크에 저장하는 편이 나을 수 있습니다.

이 PEP는 몇몇 중요한 경우에 itertools.tee가 최소한의 메모리 비용으로 작업을 수행할 수 있게 해 주는 또 다른 아이디어를 제안합니다. 사용자 코드도 때때로 이 아이디어를 활용하여 이터레이터를 복사할지, 이터레이터로부터 리스트를 만들지, 보조 디스크 파일을 사용할지 결정할 수 있습니다.

핵심 고려 사항은 내장 함수 iter가 시퀀스에 대해 만드는 이터레이터처럼 일부 중요한 이터레이터는 본질적으로 복사하기 쉽다는 점입니다. 같은 시퀀스에 대한 참조를 하나 더 얻고, 정수 인덱스를 복사하기만 하면 됩니다. 하지만 Python 2.3에서 이러한 이터레이터는 상태를 노출하지 않으며 copy.copy도 지원하지 않습니다.

따라서 이 PEP의 목적은 그러한 이터레이터 타입들이 적절한 __copy__ 메서드를 노출하도록 하는 것입니다. 마찬가지로, 사용자가 작성한 이터레이터 타입 중 제한된 시간 및 공간 비용으로 별개의 독립적인 반복에 적합한 인스턴스 복사본을 제공할 수 있는 타입도 적절한 __copy__ 메서드를 노출해야 합니다. copy.copy는 타입이 자기 인스턴스의 복사 방식을 제어할 수 있는 다른 방법도 지원하지만, 단순성을 위해 복사를 지원하는 이터레이터 타입은 copy.copy가 지원하는 다른 방법이 아니라 항상 __copy__ 메서드를 노출하는 방식으로 복사를 지원할 것을 제안합니다.

가능한 경우 이터레이터가 적절한 __copy__를 노출하도록 하면, 다음과 같이 itertools.tee 및 이와 유사한 사용자 코드를 쉽게 최적화할 수 있습니다.:

def tee(it):
    it = iter(it)
    try: copier = it.__copy__
    except AttributeError:
        # non-copyable iterator, do all the needed hard work
        # [snipped!]
    else:
        return it, copier()

이 함수는 “copy.copy(it)”를 호출하지 않는다는 점에 유의하십시오. 이 호출은 (이 PEP가 구현된 후에도) 사용자가 작성한 클래스로 구현된 어떤 이터레이터 타입에서는 여전히 “우연히 성공할” 수도 있습니다. 하지만 그 결과로 적절한 “독립적으로 이터러블한” 복사 객체를 실제로 제공하지는 않습니다.

명세

모든 이터레이터 타입 X는 X의 임의의 인스턴스 x에서 인자 없이 호출할 수 있는 호출 가능 객체인 __copy__ 메서드를 노출할 수 있습니다. 이 메서드는 이터레이터 타입이 합리적으로 적은 계산 및 메모리 비용으로 복사 가능성을 제공할 수 있는 경우에, 그리고 그 경우에만 노출되어야 합니다. 또한 __copy__ 메서드가 반환하는 새 객체 y는 x와 분리되어 독립적으로 순회할 수 있는 이터러블인 X의 새 인스턴스여야 하며, 항목들의 같은 “기반 시퀀스”를 따라 진행해야 합니다.

예를 들어, 클래스 Iter가 시퀀스를 반복하는 내장 함수 iter의 기능을 사실상 그대로 재현한다고 가정해 봅시다.:

class Iter(object):

    def __init__(self, sequence):
        self.sequence = sequence
        self.index = 0

    def __iter__(self):
        return self

    def next(self):
        try: result = self.sequence[self.index]
        except IndexError: raise StopIteration
        self.index += 1
        return result

이 Iter 클래스가 이 PEP를 준수하도록 하려면 클래스 Iter의 본문에 다음을 추가하는 것으로 충분합니다.:

def __copy__(self):
    result = self.__class__(self.sequence)
    result.index = self.index
    return result

이 경우 __copy__는 시퀀스를 복사하려고 시도조차 하지 않는다는 점에 유의하십시오. 원본 이터레이터와 복사된 이터레이터 중 하나 또는 둘 모두가 아직 시퀀스를 따라 진행하는 도중에 시퀀스가 변경되면, 어차피 이터레이션 동작이 잘못될 가능성이 매우 큽니다. 변경 가능한 시퀀스를 이터레이션하는 이터레이터에 대한 이런 일반적인 Python 동작을 바꾸는 것은 __copy__의 책임이 아닙니다(그것은 어쩌면 이터레이터의 __deepcopy__ 메서드의 명세가 될 수도 있겠지만, 이 PEP에서는 다루지 않습니다).

또한 주어진 인자로 호출되는 random 인스턴스의 어떤 메서드로부터 끝나지 않는 결과 시퀀스를 제공하는 “랜덤 이터레이터”도 생각해 봅시다.:

class RandomIterator(object):

    def __init__(self, bound_method, *args):
        self.call = bound_method
        self.args = args

    def __iter__(self):
        return self

    def next(self):
        return self.call(*self.args)

    def __copy__(self):
        import copy, new
        im_self = copy.copy(self.call.im_self)
        method = new.instancemethod(self.call.im_func, im_self)
        return self.__class__(method, *self.args)

이 이터레이터 타입은 이름이 암시하는 것보다 약간 더 일반적입니다. 임의의 바운드 메서드(또는 다른 호출 가능 객체도 가능하지만, 호출 가능 객체가 바운드 메서드가 아니면 __copy__ 메서드는 실패합니다)에 대한 호출을 지원하기 때문입니다. 하지만 사용 사례는 다음과 같이 랜덤 스트림을 생성하기 위한 것입니다.:

import random

def show5(it):
    for i, result in enumerate(it):
        print '%6.3f'%result,
        if i==4: break
    print

normit = RandomIterator(random.Random().gauss, 0, 1)
show5(normit)
copit = normit.__copy__()
show5(normit)
show5(copit)

이는 다음과 같은 출력을 표시합니다.:

-0.536  1.936 -1.182 -1.690 -1.184
 0.666 -0.701  1.214  0.348  1.373
 0.666 -0.701  1.214  0.348  1.373

핵심은 두 번째 줄과 세 번째 줄이 같다는 점이며, 이는 normit 이터레이터와 copit 이터레이터가 동일한 “기반 시퀀스”를 따라 진행하기 때문입니다. (덧붙여, self.call.im_self의 복사본을 얻으려면 __copy__ 메서드에 직접 접근하려 하지 말고 반드시 copy.copy를 사용해야 한다는 점에 유의하십시오. 예를 들어 random.Random의 인스턴스는 __copy__가 아니라 __getstate__와 __setstate__를 통해 복사를 지원하기 때문입니다. 실제로 copy.copy를 사용하는 것은 어떤 객체든 얕은 복사본을 얻는 일반적인 방법입니다. 복사 가능한 이터레이터가 이와 다른 것은, 앞서 언급했듯이 copy.copy의 결과가 이러한 “복사 가능한 이터레이터” 명세를 만족하는지 불확실하기 때문입니다).

세부 사항

이 PEP의 구현은 사용자가 작성한 이터레이터 타입이 __copy__ 메서드를 지원하도록 (단, 메모리와 런타임 측면에서 적은 비용으로 구현할 수 있고 이터레이터 객체의 독립적으로 순회 가능한 이터러블 복사본을 만들어 낼 수 있는 경우에 한해) 권고하는 내용을 Python 문서에 추가하는 것 외에도, 내장 iter가 반환하는 시퀀스에 대한 이터레이터와 내장 타입 dict의 __iter__, iterkeys, itervalues, iteritems 메서드가 반환하는 딕셔너리에 대한 이터레이터에 복사 가능성을 추가하는 것을 구체적으로 포함합니다.

제너레이터 함수가 생성하는 이터레이터는 복사할 수 없습니다. 하지만 Python 2.4의 새로운 “제너레이터 표현식”(PEP 289)이 만드는 이터레이터는 기반이 되는 iterator[s]가 복사 가능하다면 복사 가능해야 합니다. 제너레이터의 훨씬 더 폭넓은 일반성과 비교하면 제너레이터 표현식에서 할 수 있는 일에는 엄격한 제약이 있으므로, 이를 실현할 수 있을 것입니다. 마찬가지로 내장 함수 enumerate와 itertools 모듈이 제공하는 일부 함수가 생성하는 이터레이터도 기반 이터레이터가 복사 가능하다면 복사 가능해야 합니다.

이 PEP의 구현에는 동기 절에서 언급한 새로운 itertools.tee 함수의 최적화도 포함됩니다.

근거

이터레이터의 (얕은) 복사의 주요 사용 사례는 itertools.tee 함수(2.4에서 새로 추가됨)의 사용 사례와 같습니다. 사용자 코드는 이터레이터를 직접 복사하려고 시도하지 않을 것입니다. 그렇게 하면 복사할 수 없는 경우를 별도로 처리해야 하기 때문입니다. itertools.tee를 호출하면 적절한 경우 내부적으로 복사를 수행하고, 복사할 수 없는 이터레이터에 대해서는 암묵적으로 최대한 효율적인 비복사 전략으로 대체합니다. (때로는 사용자 코드가 더 직접적인 제어를 원할 수도 있는데, 특히 리스트를 만들거나 시퀀스를 디스크에 저장하는 것과 같은 다른 전략으로 복사할 수 없는 이터레이터를 처리하려는 경우입니다).

tee로 분기된 이터레이터는 “기준점” 역할을 하여 시퀀스 처리를 알려진 지점에서 계속하거나 재개할 수 있게 하고, 그동안 다른 독립적인 이터레이터는 필요에 따라 시퀀스의 더 뒷부분을 “탐색”하기 위해 자유롭게 진행할 수 있습니다. 간단한 예를 들어 보겠습니다. (양수라고 가정한) 숫자들의 이터레이터가 주어지면, 입력 이터레이터의 각 항목에 대응하여 전체 합계에서 해당 항목이 차지하는 비율을 각 항목으로 갖는 이터레이터를 반환하는 제너레이터 함수입니다. 호출자는 합계를 미리 알고 있다면 값으로 전달할 수 있습니다. 그렇지 않으면 이 제너레이터 함수를 호출하여 반환된 이터레이터가 먼저 합계를 계산합니다.

def fractions(numbers, total=None):
    if total is None:
        numbers, aux = itertools.tee(numbers)
        total = sum(aux)
    total = float(total)
    for item in numbers:
        yield item / total

numbers 이터레이터를 tee할 수 있으므로, 이 제너레이터는 필요한 경우 합계를 미리 계산할 수 있으며, numbers 이터레이터가 복사 가능하다면 반드시 O(N)의 보조 메모리를 필요로 하지는 않습니다.

“이터레이터 북마킹”의 또 다른 예로, 숫자 스트림 사이에 이따금 문자열이 “후위 연산자”로 끼어 있는 경우를 생각해 봅시다. 이런 연산자 중 단연 가장 흔한 것은 ‘+’이며, 이 연산자를 만나면 앞선 모든 숫자(직전 연산자가 있으면 그 이후의 숫자, 없으면 처음부터의 숫자)를 더해 그 결과를 내놓아야 합니다. 가끔은 ‘+’ 대신 ‘*’를 만나는데, 이때도 처리 방식은 같지만 앞선 숫자들을 더하는 대신 곱해야 합니다.

def filter_weird_stream(stream):
    it = iter(stream)
    while True:
        it, bookmark = itertools.tee(it)
        total = 0
        for item in it:
            if item=='+':
                yield total
                break
            elif item=='*':
                product = 1
                for item in bookmark:
                    if item=='*':
                        yield product
                        break
                    else:
                        product *= item
           else:
               total += item

itertools.tee의 비슷한 사용 사례로 이터레이터로 표현된 명령 스트림에 대한 “실행 취소(undo)”나 토큰 스트림 파싱에서의 “백트래킹” 같은 작업을 지원할 수 있습니다. (물론 어느 경우든 작업의 세부 사항에 따라, 이터레이터 하나만으로 시퀀스를 따라 진행하면서 시퀀스의 관련 부분을 리스트에 저장하는 것처럼 더 단순한 방법도 고려해야 합니다.)

다음은 기반 이터레이터가 __copy__를 지원하는 경우 ‘enumerate’ 내장이 __copy__를 지원하도록 확장할 수 있는 방법을 순수 Python으로 보여 주는 예입니다.:

class enumerate(object):

    def __init__(self, it):
        self.it = iter(it)
        self.i = -1

    def __iter__(self):
        return self

    def next(self):
        self.i += 1
        return self.i, self.it.next()

    def __copy__(self):
        result = self.__class__.__new__()
        result.it = self.it.__copy__()
        result.i = self.i
        return result

다음은 이터레이터의 “우연한 복사 가능성” 때문에 생기는 “취약성”의 예입니다. 이것이 copy.copy를 사용하면서, 성공하기만 하면 원본과 독립적으로 이터러블한 이터레이터를 결과로 받으리라고 기대해서는 안 되는 이유입니다. 다음은 “트리”를 (전위 순회로) 순회하는 이터레이터 클래스입니다. 여기서 트리는 단순화를 위해 그냥 중첩 리스트이며, 리스트인 항목은 서브트리로, 그 밖의 항목은 리프로 취급합니다.

class ListreeIter(object):

    def __init__(self, tree):
        self.tree = [tree]
        self.indx = [-1]

    def __iter__(self):
        return self

    def next(self):
        if not self.indx:
            raise StopIteration
        self.indx[-1] += 1
        try:
            result = self.tree[-1][self.indx[-1]]
        except IndexError:
            self.tree.pop()
            self.indx.pop()
            return self.next()
        if type(result) is not list:
            return result
        self.tree.append(result)
        self.indx.append(-1)
        return self.next()

이제, 예를 들어 다음 코드는:

import copy
x = [ [1,2,3], [4, 5, [6, 7, 8], 9], 10, 11, [12] ]

print 'showing all items:',
it = ListreeIter(x)
for i in it:
    print i,
    if i==6: cop = copy.copy(it)
print

print 'showing items >6 again:'
for i in cop: print i,
print

의도한 대로 동작하지 않습니다. “cop” 이터레이터도 원래의 “it” 이터레이터와 함께 한 단계씩 소비되어 결국 소진됩니다. copy.copy가 (의도적이 아니라) 우연히 수행하는 복사가 “index” 리스트를 복제하지 않고 공유하기 때문입니다. 이 리스트는 가변 속성인 it.indx (숫자 인덱스의 리스트)입니다. 따라서 이터레이터에 copy.copy를 적용하여 시퀀스의 일부를 두 번 순회하려고 하는 이 이터레이터의 “클라이언트 코드”는 올바르지 않습니다.

올바른 해결책으로는 itertools.tee를 사용하는 방법이 있습니다. 즉, 첫 번째 for 루프를 다음과 같이 바꾸는 것입니다:

for i in it:
    print i,
    if i==6:
        it, cop = itertools.tee(it)
        break
for i in it: print i,

(루프를 반드시 둘로 나누어야 한다는 점에 주의하십시오. 그러지 않으면 여전히 it의 원래 값으로 루프를 돌게 되는데, tee를 호출한 뒤에는 이 원래 값을 절대 다시 사용해서는 안 됩니다!!!) 또는 리스트를 만드는 방법도 있습니다. 즉,

for i in it:
    print i,
    if i==6:
        cop = lit = list(it)
        break
for i in lit: print i,

(이번에도 루프를 둘로 나누어야 합니다. list(it) 호출로 이터레이터 ‘it’이 소진되기 때문입니다.)

마지막으로, 이 PEP가 권장하는 대로 Listiter가 적절한 __copy__ 메서드를 제공한다면 이 해결책들은 모두 동작합니다:

def __copy__(self):
    result = self.__class__.new()
    result.tree = copy.copy(self.tree)
    result.indx = copy.copy(self.indx)
    return result

복사를 더 “깊게” 할 필요는 없지만, “올바른” (독립적으로 순회 가능한 이터러블) 이터레이터 복사본을 얻으려면 변경 가능한 두 “인덱스 상태” 속성은 반드시 복사해야 합니다.

권장하는 해결책은 Listiter 클래스가 이 __copy__ 메서드를 제공하도록 하는 동시에, 클라이언트 코드가 (위에서 본 것처럼 루프를 둘로 나누어) itertools.tee를 사용하도록 하는 것입니다. 이렇게 하면 클라이언트 코드가 사용할 수 있는 다양한 이터레이터 타입을 최대한 폭넓게 수용하는 동시에, 이 특정 이터레이터 타입을 tee할 때 좋은 성능도 얻을 수 있습니다.

참고 문헌

[1] python-dev에서 다음 게시물부터 시작된 논의: https://mail.python.org/pipermail/python-dev/2003-October/038969.html

[2] 표준 라이브러리 copy 모듈의 온라인 문서: https://docs.python.org/release/2.6/library/copy.html