이터레이터(iterator)는 값을 차례대로 꺼낼수 있는 객체(object)를 말한다.
우리는 지금까지 for
문을 사용할 때 range
를 사용했다. 만약 100번 반복한다면 for i in range(100):
처럼 만들어서 사용했다. 이처럼 사용할 때 0부터 99까지 연속된 숫자를 만들어낸다고 했는데, 사실 숫자를 모두 만들어내는 것이 아니라 0부터 99까지 차례대로 넣을 수 있는 이터레이터 하나만 만들어낸다. 이후 반복할 때 마다 이터레이터에서 숫자 하나씩 꺼내서 반복한다.
만약 연속된 숫자를 미리 만들어놓으면 숫자가 적을 때는 상관없지만 아주 많으면 너무 큰 메모리 공간을 낭비하게 된다. 그래서 파이썬에서는 이터레이터를 생성하고 값이 필요한 시점에 값을 만드는 방식을 사용한다. 즉, 데이터 생성을 뒤로 미루는 것인데 이러한 방식을 지연 평가(lazy evaluation)이라고 한다.
참고로 이터레이터는 반복자라고도 하는데 앞으로 아래에서는 이터레이터라고 부르겠다.
이터레이터를 만들기 앞서 반복 가능한 객체(iterable)에 대해서 먼저 알아보자. 반복 가능한 객체는 말 그대로 반복할 수 있는 객체를 말한다. 우리가 흔히 사용하는 리스트, 문자열, 딕셔너리, 세트가 반복 가능한 객체이다. 즉, 요소가 여러개 들어있고 한 번에 하나 씩 꺼내서 사용할 수 있는 객체이다.
반복 가능한 객체인지 알아보는 방법으로는 객체에 __iter__
메서드가 있는지 확인해보면 된다. 다음과 같이 dir
함수를 사용하면 객체의 메서드를 확인할 수 있다.
>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
'__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__',
'__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__',
'__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index',
'insert', 'pop', 'remove', 'reverse', 'sort']
리스트 [1, 2, 3]
을 dir
함수로 확인해보면 __iter__
메서드가 있는 것을 볼 수 있다. 이 리스트의 __iter__
메서드를 호출해보면 이터레이터가 나온다.
>>> [1, 2, 3].__iter__()
<list_iterator object at 0x03616630>
리스트의 이터레이터를 변수에 저장한 다음에 __next__
메서드를 호출해보면 요소들을 차례대로 꺼낼 수 있다.
>>> it = [1, 2, 3].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<pyshell#48>", line 1, in <module>
it.__next__()
StopIteration
이처럼 이터레이터는 __next__
로 차례대로 요소를 꺼내다가 꺼낼 요소가 없으면 StopIteration
에러를 발생시켜서 끝낸다.
물론 리스트 말고도 문자열, 딕셔너리, 세트도 __iter__
를 호출하면 이터레이터가 나오고 __next__
로 요소를 차례대로 꺼낼 수 있다.
>>> 'Hello, world!'.__iter__()
<str_iterator object at 0x03616770>
>>> {'a': 1, 'b': 2}.__iter__()
<dict_keyiterator object at 0x03870B10>
>>> {1, 2, 3}.__iter__()
<set_iterator object at 0x03878418>
리스트, 문자열, 딕셔너리, 세트는 요소가 눈에 보이는 반복 가능한 객체이다. 이번에는 요소가 눈에 안보이는 range
를 살펴보자. 다음과 같이 range(3)
에서 __iter__
로 이터레이터를 얻은 뒤 __next__
메서드를 호출해보자.
>>> it = range(3).__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
it.__next__()
StopIteration
이제 for
에 반복 가능한 객체를 사용했을 때 일어나는 과정에 대해서 살펴보자. 다음과 같이 for
에 range(3)
을 하면 우선 range
에서 __iter__
로 이터레이터를 얻는다. 그리고 한 번 반복될 때 마다 이터레이터에서 __next__
로 숫자를 꺼내서 i
에 넣고 지정된 숫자가 3
이 되면 StopIteration
를 발생시켜서 반복을 끝낸다.
이처럼 반복 가능한 객체는 __iter__
로 이터레이터를 얻고 이터레이터의 __next__
메서드를 반복한다. 여기에서는 반복 가능한 객체와 이터레이터가 분리되어 있지만(??) 클래스에 __iter__
와 __next__
메서드를 모두 구현하면 이터레이터를 만들 수 있다. 특히 __iter__
와 __next__
를 가진 객체를 이터레이터 프로토콜(iterator protocol)을 지원한다고 말한다.
정리하자면 반복 가능한 객체는 요소를 한 번에 하나씩 가져올 수 있는 객체를 말하고 이터레이터는 __next__
메서드를 사용해서 차례대로 값을 꺼낼 수 있는 객체를 말한다. 반복 가능한 객체(iterable) 와 이터레이터(iterator)는 별개의 객체이므로 둘은 구분해야 한다. 즉, 반복 가능한 객체에서 __iter__
메서드로 이터레이터를 얻는다.
앞서 시퀀스 객체를 배울 때 리스트, 튜플, range
, 문자열은 시퀀스 객체라고 했는데 이번 단원에서는 반복 가능한 객체라고 했다. 이 둘의 차이점이 무엇일까?
반복 가능한 객체는 시퀀스 객체를 포함한다.
리스트, 튜플, range
, 문자열은 반복 가능한 객체이면서 시퀀스 객체이다. 하지만 딕셔너리, 세트는 반복 가능한 객체이지만 시퀀스 객체는 아니다. 왜냐하면 시퀀스 객체는 요소의 순서가 정해져 있고 연속적(sequence)으로 이어져 있어야 하지만 딕셔너리와 세트는 요소(키)의 순서가 정해져 있지 않기 때문이다. 따라서 시퀀스 객체가 반복 가능한 객체보다 좁은 개념이다. (시퀀스 객체 < 반복 가능한 객체)
즉, 요소의 순서가 정해져 있고 연속적으로 이어져 있으면 시퀀스 객체, 요소의 순서와 상관없이 한 번에 하나씩 꺼낼 수 있으면 반복 가능한 객체이다.
이제 __iter__
메서드와 __next__
메서드를 구현해서 이터레이터를 만들어보자. 간단하게 range
처럼 작동하는 이터레이터이다.
class Counter:
def __init__(self, stop):
self.current = 0 # 현재 숫자 유지, 0부터 지정된 숫자 직전까지 반복
self.stop = stop # 반복을 끝낼 숫자
def __iter__(self):
return self # 현재 인스턴스를 반환
def __next__(self):
if self.current < self.stop: # 현재 숫자가 반복을 끝낼 숫자보다 작을 때
r = self.current # 반환할 숫자를 변수에 저장
self.current += 1 # 현재 숫자를 1 증가시킴
return r # 숫자를 반환
else: # 현재 숫자가 반복을 끝낼 숫자보다 크거나 같을 때
raise StopIteration # 예외 발생
for i in Counter(3):
print(i, end=' ')
실행 결과
0 1 2
먼저 이터레이터를 작성하려면 __init__
메서드를 만든다. 여기서 Counter(3)
처럼 끝낼 숫자를 받았으므로 self.stop
속성에 넣어준다. 그리고 반복 할 때 마다 현재 숫자를 속성 self.current
에 0 을 넣어준다.
def __init__(self, stop):
self.current = 0 # 현재 숫자 유지, 0부터 지정된 숫자 직전까지 반복
self.stop = stop # 반복을 끝낼 숫자
그리고 __iter__
메서드를 만드는데 여기에서는 self
만 반환하면 된다. 이 객체는 리스트, 문자열, range
,딕셔너리, 세트 처럼 __iter__
을 호출해줄 반복 가능한 객체가 없으므로 현재 인스턴스를 반환하면 된다. 즉, 이 객체는 반복 가능한 객체이면서 이터레이터이다.
def __iter__(self):
return self # 현재 인스턴스를 반환
그 다음은 __next__
메서드를 만든다. 조건에 따라서 현재 숫자를 리턴하고 조건에 맞지 않으면 StropIteration
예외를 발생시켜서 종료한다.
def __next__(self):
if self.current < self.stop: # 현재 숫자가 반복을 끝낼 숫자보다 작을 때
r = self.current # 반환할 숫자를 변수에 저장
self.current += 1 # 현재 숫자를 1 증가시킴
return r # 숫자를 반환
else: # 현재 숫자가 반복을 끝낼 숫자보다 크거나 같을 때
raise StopIteration # 예외 발생
지금까지 간단한 이터레이터를 만들어봤다. 여기서 주의할 점은 __init__
메서드에서 초기값, __next__
메서드에서 조건식과 현재값 부분이다. 여기가 잘못되면 미묘한 버그가 생길 수 있다 예를 들어서 0, 1, 2 가 출력되어야 하는데 1, 2, 3 이 출력된다거나 0, 1, 2, 3 이 출력될 수 있다.
참고로 이터레이터는 언패킹(unpacking)이 가능하다. 따라서 다음과 같이 한 번에 변수 여러개에 값을 할당할 수 있다. 물론 이터레이터가 반복하는 횟수와 변수의 개수가 동일해야하 한다.
>>> a, b, c = Counter(3)
>>> print(a, b, c)
0 1 2
>>> a, b, c, d, e = Counter(5)
>>> print(a, b, c, d, e)
0 1 2 3 4
사실 우리가 자주 사용하는 map
도 이터레이터이다. 그래서 a,b,c = map(int, input().split())
과 같이 언패킹도 가능했다.
함수를 호출한 뒤에 반환값을 저장할 때 _
에 저장하는 경우가 종종 있다.
>>> _, b = range(2)
>>> b
1
사실 이 코드는 a, b = range(2)
와 동일하다. 그런데 a
는 사용할 필요가 없을 때 반환값을 사용하지 않고 무시하겠다는 관례적 표현으로 _
에 할당한다.
지금까지는 __iter__
, __next__
메서드를 구현하는 방식으로 이터레이터를 만들었는데 이번에는 __getitem__
메서드를 구현하여 인덱스에 접근할 수 있는 이터레이터를 만들어보자.
앞서 만든 Counter
이터레이터를 인덱스로 접근할 수 있게 다시 만들어보자.
class 이터레이터이름:
def __getitem__(self, 인덱스):
코드
class Counter:
def __init__(self, stop):
self.stop = stop
def __getitem__(self, index):
if index < self.stop:
return index
else:
raise IndexError
print(Counter(3)[0], Counter(3)[1], Counter(3)[2])
for i in Counter(3):
print(i, end=' ')
실행 결과
0 1 2
0 1 2
소스코드를 잘 보면 __init__
, __getitem__
만 있는데도 잘 작동된다. 클래스에서 __getitem__
만 구현되어 있으면 이터레이터가 되고 __iter__
, __next__
메서드는 생략해도 된다(초기값이 없으면 __init__
도 생략 가능).
그러면 코드를 다시 한 번 살펴보자. __init__
에서는 stop
속성만 만들고 current
속성은 만들지 않았다.
class Counter:
def __init__(self, stop):
self.stop = stop # 반복을 끝낼 숫자
이제 클래스에서 __getitem__
메서드만 구현하면 인덱스로 접근할 수 있는 이터레이터가 된다. 먼저 __getitem__
는 index
를 받는데 index
가 self.stop
보다 작으면 index
를 리턴하고 아니면 IndexError
예외를 발생시킨다.
def __getitem__(self, index): # 인덱스를 받음
if index < self.stop: # 인덱스가 반복을 끝낼 숫자보다 작을 때
return index # 인덱스를 반환
else: # 인덱스가 반복을 끝낼 숫자보다 크거나 같을 때
raise IndexError # 예외 발생
이렇게 하면 Counter(3)[0]
처럼 인덱스로 이터레이터에 접근할 수 있다.
이번에는 파이썬 내장 함수 iter
과 next
에 대해서 알아보자. iter
는 객체의 __iter__
를 호출해주고 next
는 __next__
를 호출해준다. 그러면 range(3)
에서 이 두 함수를 활용해보자.
>>> it = iter(range(3))
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
next(it)
StopIteration
iter
는 반복을 끝낼 값을 지정하면 지정한 값이 나올 때 반복을 끝낸다. 이 경우에는 반복 가능한 객체 대신 호출 가능한 객체(callable) 를 넣어준다. 참고로 반복을 끝낼 값을 sentinel
이라고 하는데 감시병이라는 뜻이다. 즉, 반복을 감시하다가 특정 값이 나오면 반복을 끝낸다.
예를 들어서 random.randint(0, 5)
와 같이 0부터 5 사이의 무작이 숫자를 호출할 때 2가 나오면 반복을 끝내도록 만들 수 있다. 이때 호출 가능한 객체를 넣어야 하므로 매개 변수가 없는 함수 또는 람다 표현식을 넣은다.
>>> import random
>>> it = iter(lambda : random.randint(0, 5), 2)
>>> next(it)
0
>>> next(it)
3
>>> next(it)
1
>>> next(it)
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
next(it)
StopIteration
next(it)
으로 숫자를 계속 만들다가 2를 만나면 StopIteration
이 발생한다. 물론 숫자가 무작위로 생성되므로 호출하는 횟수가 매번 달라진다. 물론 반복문에 넣어서 사용할 수 있다.
>>> import random
>>> for i in iter(lambda : random.randint(0, 5), 2):
... print(i, end=' ')
...
3 1 4 0 5 3 3 5 0 4 1
이렇게 iter
을 사용하면 if
조건문으로 값을 확인하지 않아도 되므로 코드가 간단해진다.
import random
while True:
i = random.randint(0, 5)
if i == 2:
break
print(i, end=' ')
next
는 기본값을 지정할 수 있다. 기본값을 지정하면 반복이 끝나더라도 StopIteration
예외를 발생시키지 않고 기본값을 출력한다. 즉, 반복할 수 있을 때는 값을 출력하고 반복이 끝났으면 기본값을 출력한다.
>>> it = iter(range(3))
>>> next(it, 10)
0
>>> next(it, 10)
1
>>> next(it, 10)
2
>>> next(it, 10)
10
>>> next(it, 10)
10
지금까지 반복 가능한 객체와 이터레이터에 대해서 배웠다. 여기에서는 이터레이터를 만들 때 __iter__
, __next__
또는 __getitem__
메서드를 구현해야 한다는 점을 기억하자.