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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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 모듈에 있는 복사 메서드의 표준 메커니즘이 실제로 복사본을 만들어 반환하는 것입니다. 그러나 해당 클래스의 인스턴스에서 .next()를 호출했을 때 일부 속성을 새 값으로 재바인딩하는 것만으로 인스턴스 상태가 변경되고, 일부 속성의 기존 값은 변경하지 않는 경우에만 복사본이 원래 객체와 독립적으로 이터레이트될 수 있습니다.
예를 들어 “인덱스” 상태가 정수 속성으로 유지되는 이터레이터는 사용 가능한 복사본을 제공할 가능성이 높습니다. 정수는 변경할 수 없으므로 .next()는 아마도 해당 속성을 재바인딩하기만 하기 때문입니다. 반면 “인덱스” 상태가 리스트 속성으로 유지되는 다른 이터레이터는 .next()가 실행될 때 동일한 리스트 객체를 변경할 가능성이 높으므로, 그러한 이터레이터의 복사본은 원래 이터레이터와 별도로 독립적으로 이터레이트할 수 없습니다.
이러한 기존 상황을 고려하면 일부 이터레이터 객체에 대한 copy.copy(it)는 그다지 유용하지 않으며, 따라서 널리 사용되지도 않습니다. 그러나 이터레이터의 “스냅샷”을 “북마크”로 가져와서 시퀀스를 계속 따라가며 이터레이트하다가 나중에 북마크 이후부터 동일한 시퀀스를 다시 이터레이트할 수 있으면 유용한 경우가 많습니다. 이러한 “북마크 기능”을 지원하기 위해 2.4에서 itertools 모듈에 ‘tee’ 함수가 추가되었으며, 다음과 같이 사용합니다.:
it, bookmark = itertools.tee(it)
‘it’의 이전 값은 다시 사용해서는 안 되므로, 이러한 일반적인 사용 관용구에서는 이름을 재바인딩합니다. 이 호출 후 ‘it’과 ‘bookmark’는 원래 ‘it’ 값과 동일한 기반 시퀀스에 대해 독립적으로 이터레이트할 수 있는 이터레이터가 되며, 이는 “이터레이터 복사”에 대한 애플리케이션의 요구 사항을 충족합니다.
그러나 itertools.tee는 인자로 전달된 이터레이터의 특성에 대해 어떠한 가정도 할 수 없으므로, 두 ‘teed’ 이터레이터 중 하나만 진행하고 아직 두 이터레이터 모두가 진행하지 않은 모든 항목을 메모리에 저장해야 합니다. 두 이터레이터의 진행 위치가 서로 크게 멀어지면 메모리 측면에서 비용이 상당히 커질 수 있습니다. 실제로 일부 경우에는 이터레이터에서 리스트를 만들어 하위 시퀀스를 반복해서 진행할 수 있도록 하는 편이 나을 수 있으며, 메모리 측면의 비용이 너무 크다면 항목을 디스크에 저장하여 다시 반복해서 이터레이트할 수 있도록 하는 편이 나을 수 있습니다.
이 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를 준수하도록 만들려면, class Iter의 본문에 다음을 추가하는 것으로 충분합니다:
def __copy__(self):
result = self.__class__(self.sequence)
result.index = self.index
return result
이 경우 __copy__는 시퀀스를 복사하려고 시도조차 하지 않는다는 점에 유의하십시오. 원본과 복사된 이터레이터 중 하나 또는 둘 다가 여전히 시퀀스를 진행하는 동안 시퀀스가 변경되면, 어차피 반복 동작이 잘못될 가능성이 매우 높습니다 – 가변 시퀀스를 반복하는 이터레이터에 대해 이러한 일반적인 파이썬 동작을 변경하는 것은 __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가 이러한 “복사 가능한 이터레이터” 명세를 지원하는지에 대한 불확실성 때문에 다릅니다).
세부 사항
사용자 코드 이터레이터 타입이 __copy__ 메서드를 지원하도록 권장하는 내용을 파이썬 문서에 추가하는 것 외에도(오직 메모리 및 실행 시간 비용이 적게 구현될 수 있고, 이터레이터 객체의 독립적으로 반복 가능한 복사본을 생성하는 경우에만), 이 PEP의 구현은 특히 내장 iter가 반환하는 시퀀스에 대한 이터레이터, 그리고 내장 타입 dict의 __iter__, iterkeys, itervalues, iteritems 메서드가 반환하는 딕셔너리에 대한 이터레이터에 복사 가능성을 추가하는 것을 포함할 것입니다.
제너레이터 함수가 생성하는 이터레이터는 복사 가능하지 않을 것입니다. 그러나 파이썬 2.4의 새로운 “제너레이터 표현식”(PEP 289)이 생성하는 이터레이터는 그 기저 이터레이터가 복사 가능하다면 복사 가능해야 합니다. 제너레이터 표현식에서 가능한 것에 대한 엄격한 제한은, 제너레이터의 훨씬 더 큰 일반성에 비해, 이를 실현 가능하게 만들어줄 것입니다. 마찬가지로, 내장 함수 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의 유사한 사용 사례로는 이터레이터로 표현된 명령 스트림에 대한 “실행 취소”, 토큰 스트림 파싱에 대한 “백트래킹” 등의 작업을 지원하는 것을 들 수 있습니다. (물론 각 경우마다 작업의 세부 사항에 따라, 시퀀스를 단 하나의 이터레이터로 순회하면서 관련 부분을 리스트에 저장하는 것과 같은 더 단순한 방법도 고려해야 합니다.)
다음은 ‘enumerate’ 내장 함수를, 그 기저 이터레이터가 __copy__를 지원하는 경우 이 함수도 __copy__를 지원하도록 확장하는 방법을 순수 파이썬으로 보여주는 예제입니다:
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가 성공했을 때 그 결과로 원본과 독립적으로 순회 가능한 이터레이터를 얻을 것이라 기대하며 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
의도한 대로 작동하지 않습니다 – copy.copy가 수행하는 (의도적이 아닌) 우연한 복사가, 가변 속성인 it.indx (숫자 인덱스들의 리스트)인 “인덱스” 리스트를 복제하는 대신 공유하기 때문에, “cop” 이터레이터는 원본 “it” 이터레이터와 마찬가지로 한 단계씩 소비되어 소진됩니다. 따라서 이터레이터에 대한 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 호출 이후 it의 원본 값은 더 이상 사용해서는 안 됩니다!!!); 또는 리스트를 만드는 것입니다, 즉
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
Copyright
This document has been placed in the public domain.