파이썬 이터레이터

대부분의 파이썬 튜토리얼들과는 달리 파이썬 3.3을 중심으로 기술하고 파이썬 2를 포함한 하위 버전에서 차이를 보이면 따로 설명합니다.

파이썬의 시퀀스는 for 루프로 탐색할 수 있습니다.

>>> for e in [1,2,3]: # 리스트
...     print(e)
1
2
3
>>> for e in (1,2,3): # 튜플
...     print(e)
1
2
3
>>> for e in 'abc': # 문자열
...     print(e)
a
b
c

for 루프는 시퀀스가 아닌 딕셔너리(dictionary)에서도 가능합니다.

>>> for k in {'a':1, 'b':2, 'c':3}: # 딕셔너리
...     print(k)
b
c
a

이 경우 딕셔너리의 키를 탐색하게 되는데, 키가 전달되는 순서는 정해지지 않았습니다. 때문에 여러분이 직접 실행할 경우, 위의 예와는 다른 순서로 키가 인쇄될 수 있습니다. 이터레이터(iterator)를 제공한다면 시퀀스가 아닌 타입도 for 루프로 탐색할 수 있습니다. 이터레이터는 내장 함수 iter() 를 사용해서 얻습니다.

>>> iter({})
<dict_keyiterator object at 0x108f7afc8>

이렇게 얻은 이터레이터를 탐색하려면 내장 함수 next() 를 사용하면 됩니다.

>>> it = iter({'a':1, 'b':2, 'c':3})
>>> print(next(it))
b
>>> print(next(it))
c
>>> print(next(it))
a
>>> next(it)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
StopIteration

next() 를 사용해 한 번에 하나씩 탐색해 나가다가, StopIteration 예외가 발생하면 끝냅니다. 이러한 탐색 법에 대한 약속을 이터레이터 프로토콜(Iterator Protocol)이라고 부르는데, for 루프는 간결한 문법을 제공합니다.

>>> for e in container:
...     print(e)

는 다음과 같은 while 루프와 동일한 결과를 줍니다.

>>> it = iter(container)
>>> while True:
...     try:
...         e = next(it)
...         print(e)
...     except StopIteration:
...         break

사용자가 정의한 클래스가 이터레이터를 지원하려면 __iter__() 메쏘드를 정의하면 됩니다. iter() 는 객체의 __iter__() 메쏘드가 돌려주는 값을 이터레이터로 사용합니다. next() 에 이터레이터를 전달하면 이터레이터의 __next__() 메쏘드를 호출합니다. 때문에 최소한의 이터레이터는 이렇게 구성할 수 있습니다.

>>> class Range:
...     def __init__(self, n):
...         self.n = n
...         self.c = 0
...     def __iter__(self):
...         return self # 이터레이터인 경우는 자신을 돌려주면 됩니다.
...     def __next__(self):
...         if self.c < self.n:
...             v = self.c
...             self.c += 1
...             return v
...         else:
...             raise StopIteration
...     next = __next__ # 파이썬 2 에서는 __next__ 대신 next 를 사용합니다.
>>> for x in Range(3):
...     print(x)
0
1
2

Range 클래스는 이터레이터 외의 용도가 없습니다. 하지만 딕셔너리는 별도의 용도가 있고, 사실 딕셔너리는 이터레이터가 아닙니다.

>>> next({})
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: 'dict' object is not an iterator

딕셔너리를 for 루프에 직접 사용할 수 있는 이유는 딕셔너리 역시 __iter__() 메쏘드를 제공하고 있기 때문입니다. 비슷한 방법으로 RangeFactory 라는 클래스를 만들어보면:

>>> class RangeFactory:
...     def __init__(self, n):
...         self.n = n
...     def __iter__(self):
...         return Range(self.n)
>>> for x in RangeFactory(3):
...     print(x)
0
1
2

__next__() 메쏘드를 제공하고 있지 않기 때문에 RangeFactory 의 인스턴스는 이터레이터가 아닙니다만, __iter__() 메쏘드를 통해 이터레이터(이 경우에는 Range 인스턴스)를 제공하고 있기 때문에 for 루프에서 사용될 수 있습니다. 사실은 이 용법 때문에 이터레이터에도 큰 의미 없는 __iter__() 메쏘드를 만들어주도록 요구하고 있습니다.

이렇게 for 루프에 이터레이터를 직접 요구하지 않고, 이터레이터를 만드는 객체를 요구함으로써, 많은 경우에 for 루프가 깔끔해지기는 하지만, 간혹 혼란을 야기할 수 있는 경우도 발생할 수 있습니다.

>>> r = Range(3)
>>> for x in r:
...     print(x)
...     if x == 1:
...         break
0
1
>>> for x in r:
...     print(x)
2

두 개의 for 루프는 동일한 Range 인스턴스를 사용하고 있습니다. 반면에 RangeFactory 를 사용하는 경우는:

>>> r = RangeFactory(3)
>>> for x in r:
...     print(x)
...     if x == 1:
...         break
0
1
>>> for x in r:
...     print(x)
0
1
2

이 경우는 for 루프를 시작할 때마다 새로운 이터레이터(Range 인스턴스)가 만들어집니다. 때문에 매번 새로운 루프가 시작되는 것이지요. for 루프가 이터레이터를 재사용하는 것으로 보일 때는, 인스턴스가 이터레이터인지 이터레이터를 제공하는 객체인지를 확인하는 것이 중요할 수 있습니다.

프로그래밍을 하다 보면 한쪽에서 생산하고(produce), 다른 쪽에서 소비(consume) 하는 상황을 자주 만납니다. 이를 생산자-소비자 패턴(Producer-Consumer Pattern)이라고 하는데, 이터레이터는 소비자 쪽의 코드를 함축적이면서도 자연스럽게 기술할 수 있는 방법을 제공합니다. 이제 필요한 것은 생산자 쪽의 코드를 자연스럽게 구성할 수 있는 방법입니다. 다음에 다룰 제너레이터(Generator) 가 파이썬의 대답입니다.