Unit 41. 이터레이터 사용하기

이터레이터(iterator)는 값을 차례대로 꺼낼 수 있는 객체(object)입니다.

지금까지 for 반복문을 사용할 때 range를 사용했습니다. 만약 100번을 반복한다면 for i in range(100):처럼 만들었습니다. 이 for 반복문을 설명할 때 for i in range(100):은 0부터 99까지 연속한 숫자를 만들어낸다고 했는데, 사실은 숫자를 모두 만들어 내는 것이 아니라 0부터 99까지 값을 차례대로 꺼낼 수 있는 이터레이터를 하나만 만들어냅니다. 이후 반복할 때마다 이터레이터에서 숫자를 하나씩 꺼내서 반복합니다.

만약 연속한 숫자를 미리 만들면 숫자가 적을 때는 상관없지만 숫자가 아주 많을 때는 메모리를 많이 사용하므로 성능에도 불리합니다. 그래서 파이썬에서는 이터레이터만 생성하고 값이 필요한 시점이 되었을 때 값을 만드는 방식을 사용합니다. 즉, 데이터 생성을 뒤로 미루는 것인데 이런 방식을 지연 평가(lazy evaluation)라고 합니다.

참고로 이터레이터는 반복자라고 부르기도 합니다. 이 책에서는 이터레이터를 사용하겠습니다.

41.1 반복 가능한 객체 알아보기

이터레이터를 만들기 전에 먼저 반복 가능한 객체(iterable)에 대해 알아보겠습니다. 반복 가능한 객체는 말 그대로 반복할 수 있는 객체인데 우리가 흔히 사용하는 문자열, 리스트, 딕셔너리, 세트가 반복 가능한 객체입니다. 즉, 요소가 여러 개 들어있고, 한 번에 하나씩 꺼낼 수 있는 객체입니다.

객체가 반복 가능한 객체인지 알아보는 방법은 객체에 __iter__ 메서드가 들어있는지 확인해보면 됩니다. 다음과 같이 dir 함수를 사용하면 객체의 메서드를 확인할 수 있습니다.

  • 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

it에서 __next__를 호출할 때마다 리스트에 들어있는 1, 2, 3이 나옵니다. 그리고 3 다음에 __next__를 호출하면 StopIteration 예외가 발생합니다. 즉, [1, 2, 3]이므로 1, 2, 3 세 번 반복합니다.

이처럼 이터레이터는 __next__로 요소를 계속 꺼내다가 꺼낼 요소가 없으면 StopIteration 예외를 발생시켜서 반복을 끝냅니다.

물론, 리스트뿐만 아니라 문자열, 딕셔너리, 세트도 __iter__를 호출하면 이터레이터가 나옵니다. 그리고 이터레이터에서 __next__를 호출하면 차례대로 값을 꺼냅니다(__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

it에서 __next__를 호출할 때마다 0부터 숫자가 증가해서 2까지 나왔습니다. 그리고 2 다음에 __next__를 호출했을 때 StopIteration 예외가 발생했습니다. 즉, range(3)이므로 0, 1, 2 세 번 반복하며 요소가 눈에 보이지 않지만 지정된 만큼 숫자를 꺼내서 반복할 수 있습니다.

이제 for에 반복 가능한 객체를 사용했을 때 동작 과정을 알아보겠습니다. 다음과 같이 forrange(3)을 사용했다면 먼저 range에서 __iter__로 이터레이터를 얻습니다. 그리고 한 번 반복할 때마다 이터레이터에서 __next__로 숫자를 꺼내서 i에 저장하고, 지정된 숫자 3이 되면 StopIteration을 발생시켜서 반복을 끝냅니다.

그림 41-1 for와 range의 반복 과정
그림 41 1 for와 range의 반복 과정

이처럼 반복 가능한 객체는 __iter__ 메서드로 이터레이터를 얻고, 이터레이터의 __next__ 메서드로 반복합니다. 여기서는 반복 가능한 객체와 이터레이터가 분리되어 있지만 클래스에 __iter____next__ 메서드를 모두 구현하면 이터레이터를 만들 수 있습니다. 특히 __iter__, __next__를 가진 객체를 이터레이터 프로토콜(iterator protocol)을 지원한다고 말합니다.

정리하자면 반복 가능한 객체는 요소를 한 번에 하나씩 가져올 수 있는 객체이고, 이터레이터는 __next__ 메서드를 사용해서 차례대로 값을 꺼낼 수 있는 객체입니다. 반복 가능한 객체와 이터레이터는 별개의 객체이므로 둘은 구분해야 합니다. 즉, 반복 가능한 객체에서 __iter__ 메서드로 이터레이터를 얻습니다.