27. 이터레이터(Iterator)
1. 키워드
- 이터레이터(Iterator)
- 반복 가능한 객체(Iterable)
- 호출 가능한 객체(Callable)
2. 이터레이터 사용하기
- 이터레이터는 값을 차례대로 꺼낼 수 있는 객체를 말한다.
- 지금까지
for
반복문을 사용할 때range
를 사용했다. - 만약
100
번을 반복한다면for i in range(100):
처럼 만들었다. - 이
for
반복문을 설명할 때for i in range(100):
은0
부터99
까지 연속된 숫자를 만들어낸다고 했는데, 사실은 숫자를 모두 만들어내는 것이 아니라0
부터99
까지 값을 차례대로 꺼낼 수 있는 이터레이터를 하나만 만들어낸다. - 이후 반복할 때마다 이터레이터에서 숫자를 하나씩 꺼내서 반복한다.
- 만약 연속된 숫자를 미리 만들면 숫자가 적을 때는 상관없지만 숫자가 아주 많을 때는 메모리를 많이 사용하게 되므로 성능에도 불리하다.
- 그래서 파이썬에서는 이터레이터만 생성하고 값이 필요한 시점이 되었을 때 값을 만드는 방식을 사용한다.
- 즉, 데이터 생성을 뒤로 미루는 것인데 이런 방식을 지연 평가(Lazy Evaluation)라고 한다.
3. 반복 가능한 객체 알아보기
- 이터레이터를 만들기 전에 먼저 반복 가능한 객체에 대해 알아보자.
- 반복 가능한 객체는 말 그대로 반복할 수 있는 객체인데, 흔히 사용하는 문자열, 리스트, 딕셔너리, 세트가 반복 가능한 객체이다.
- 즉, 요소가 여러 개 들어있고 한 번에 하나씩 꺼낼 수 있는 객체이다.
- 객체가 반복 가능한 객체인지 알아보는 방법은 객체에
__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__
를 호출해 보면 이터레이터가 나온다.
- 리스트의 이터레이터를 변수에 저장한 뒤
__next__
메서드를 호출해 보면 요소를 차례대로 꺼낼 수 있다.
>>> it = [1, 2, 3].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__() # StopIteration
it
에서__next__
를 호출할 때마다 리스트에 들어있는1
,2
,3
이 나온다.- 그리고
3
다음에__next__
를 호출하면StopIteration
예외가 발생한다. - 즉,
[1, 2, 3]
이므로1
,2
,3
세 번 반복한다. - 이처럼 이터레이터는
__next__
로 요소를 계속 꺼내다가 꺼낼 요소가 없으면StopIteration
예외를 발생시켜서 반복을 끝낸다.
- 물론, 리스트뿐만 아니라 문자열, 딕셔너리, 세트도
__iter__
를 호출하면 이터레이터가 나온다.
>>> "Hello, world!".__iter__()
<str_iterator object at ...>
>>> {"a": 1, "b": 2}.__iter__()
<dict_keyiterator object at ...>
>>> {1, 2, 3}.__iter__()
<set_iterator object at ...>
- 리스트, 문자열, 딕셔너리, 세트는 요소가 눈에 보이는 반복 가능한 객체이다.
- 이번에는 요소가 눈에 보이지 않는
range
를 살펴보자.
- 다음과 같이
range(3)
에서__iter__
로 이터레이터를 얻어낸 뒤__next__
메서드를 호출해 본다.
>>> it = range(3).__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__() # StopIteration
it
에서__next__
를 호출할 때마다0
부터 숫자가 증가해서2
까지 나왔다.- 그리고
2
다음에__next__
를 호출했을 때StopIteration
예외가 발생했다. - 즉,
range(3)
이므로0
,1
,2
세 번 반복하며 요소가 눈에 보이지 않지만 지정된 만큼 숫자를 꺼내서 반복할 수 있다.
1) for
와 반복 가능한 객체
- 이제
for
에 반복 가능한 객체를 사용했을 때 동작 과정을 알아보자.
- 다음과 같이
for
에range(3)
을 사용했다면 먼저range
에서__iter__
로 이터레이터를 얻는다. - 그리고 한 번 반복할 때마다 이터레이터에서
__next__
로 숫자를 꺼내서i
에 저장하고, 지정된 숫자3
이 되면StopIteration
을 발생시켜서 반복을 끝낸다.
- 이처럼 반복 가능한 객체는
__iter__
메서드로 이터레이터를 얻고, 이터레이터의__next__
메서드로 반복한다. - 여기서 반복 가능한 객체와 이터레이터가 분리되어 있지만 클래스에
__iter__
와__next__
메서드를 모두 구현하면 이터레이터를 만들 수 있다. - 특히
__iter__
,__next__
를 가진 객체를 이터레이터 프로토콜(Iterator Protocol)을 지원한다고 말한다. - 정리하자면 반복 가능한 객체는 요소를 한 번에 하나씩 가져올 수 있는 객체이고, 이터레이터는
__next__
메서드를 사용해서 차례대로 값을 꺼낼 수 있는 객체이다. - 즉, 반복 가능한 객체에서
__iter__
메서드로 이터레이터를 얻는다.
시퀀스 객체와 반복 가능한 객체의 차이
- 다음 그림과 같이 반복 가능한 객체는 시퀀스 객체를 포함한다.
- 리스트, 튜플,
range
, 문자열은 반복 가능한 객체이면서 시퀀스 객체이다. - 하지만, 딕셔너리와 세트는 반복 가능한 객체이지만 시퀀스 객체는 아니다.
- 왜냐하면 시퀀스 객체는 요소의 순서가 정해져 있고 연속적(Sequence)으로 이어져 있어야 하는데, 딕셔너리와 세트는 요소(키)의 순서가 정해져 있지 않기 때문이다.
- 따라서 시퀀스 객체가 반복 가능한 객체보다 좁은 개념이다.
- 즉, 요소의 순서가 정해져 있고 연속적으로 이어져 있으면 시퀀스 객체, 요소의 순서와는 상관없이 요소를 한 번에 하나씩 꺼낼 수 있으면 반복 가능한 객체이다.
4. 이터레이터 만들기
- 이제
__iter__
,__next__
메서드를 구현해서 직접 이터레이터를 만들어보자.
- 간단하게
range(횟수)
처럼 동작하는 이터레이터이다.
class Counter:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current < self.stop:
r = self.current
self.current += 1
return r
else:
raise StopIteration
for i in Counter(3):
print(i, end=" ")
# 0 1 2
- 실행을 해보면
0 1 2
가 나온다. - 이렇게
0
부터 지정된 숫자 직전까지 반복하는 이터레이터Counter
를 정의했다.
- 먼저 클래스로 이터레이터를 작성하려면
__init__
메서드를 만든다. - 여기서는
Counter(3)
처럼 반복을 끝낼 숫자를 받았으므로self.stop
에stop
을 넣어준다. - 그리고 반복할 때마다 현재 숫자를 유지해야 하므로 속성
self.current
에0
을 넣어준다.
- 그리고
__iter__
메서드를 만드는데 여기서는self
만 반환하면 끝이다. - 이 객체는 리스트, 문자열, 딕셔너리, 세트,
range
처럼__iter__
를 호출해 줄 반복 가능한 객체가 없으므로 현재 인스턴스를 반환하면 된다. - 즉, 이 객체는 반복 가능한 객체이면서 이터레이터이다.
- 그다음에
__next__
메서드를 만든다. __next__
에서는 조건에 따라 숫자를 만들어내거나StopIteration
예외를 발생시킨다.- 현재 숫자
self.current
가 반복을 끝낼 숫자self.stop
보다 작을 때는self.current
를1
증가시키고 현재 숫자를 반환한다. - 이때
1
증가한 숫자를 반환하지 않도록 숫자를 증가시키기 전에r = self.current
처럼 반환할 숫자를 변수에 저장해 놓는다. - 그다음에
self.current
가self.stop
보다 크거나 같아질 때는raise StopIteration
으로 예외를 발생시킨다.
def __next__(self):
if self.current < self.stop:
r = self.current
self.current += 1
return r
else:
raise StopIteration
for
반복문에Counter(3)
을 지정해서 실행해 보면3
번 반복하면서0 1 2
가 출력된다.
- 이터레이터를 만들 때는
__init__
메서드에서 초깃값,__next__
메서드에서 조건식과 현재값 부분을 주의해야 한다.
1) 이터레이터 언패킹
- 이터레이터는 언패킹이 가능하며, 다음과 같이
Counter()
의 결과를 변수 여러 개에 할당할 수 있다.
>>> 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))
처럼 언패킹으로 변수 여러 개에 값을 할당할 수 있다.
반환값을 _
에 저장하는 이유
- 함수를 호출한 뒤 반환값을 저장할 때
_
(밑줄 문자)를 사용하는 경우가 있다.
- 반환값을 언패킹했을 때
_
에 할당하는 것은 특정 순서의 반환값을 사용하지 않고 무시하겠다는 관례적 표현이다.
- 예를 들어 다음과 같은 코드는 언패킹했을 때 두 번째 변수는 사용하지 않겠다는 뜻이다.
5. 인덱스로 접근할 수 있는 이터레이터 만들기
- 이번에는
__getitem__
메서드를 구현하여 인덱스로 접근할 수 있는 이터레이터를 만들어보자.
- 앞에서 만든
Counter
이터레이터를 인덱스로 접근할 수 있도록 다시 만들어보자.
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
Counter(3)[0]
을 출력했을 때0
이 나왔다.- 마찬가지로
Counter(3)[1]
은1
,Counter(3)[2]
는2
가 나왔다. - 그리고
for
반복문에Counter
를 사용했을 때도1
,2
,3
이 나왔다. - 소스 코드를 잘 보면
__init__
메서드와__getitem__
메서드만 있는데도 동작이 잘 된다. - 클래스에서
__getitem__
만 구현해도 이터레이터가 되며__iter__
,__next__
는 생략해도 된다. - 물론, 초깃값이 필요없다면
__init__
도 생략 가능하다.
- 그럼
__init__
메서드부터 살펴보자. - 여기서는
Counter(3)
처럼 반복을 끝낼 숫자를 받았으므로self.stop
에stop
을 넣어준다.
- 이제 클래스에서
__getitem__
메서드를 구현하면 인덱스로 접근할 수 있는 이터레이터가 된다. - 먼저
__getitem__
은 매개변수로 인덱스 index를 받는다. - 그리고 지정된
index
가 반복을 끝낼 숫자self.stop
보다 작을 때index
를 반환한다. index
가self.stop
보다 크거나 같으면IndexError
를 발생시킨다.- 즉,
Counter(3)
과 같이 반복을 끝낼 숫자가3
이면 인덱스는2
까지 지정할 수 있다.
- 이렇게 하면
Counter(3)[0]
처럼 이터레이터를 인덱스로 접근할 수 있다. - 참고로 여기서는 반복할 숫자와 인덱스가 같아서
index
를 그대로 반환했지만,index
와 식을 조합해서 다른 숫자를 만드는 방식으로 활용할 수 있다. - 예를 들어
index * 10
을 반환하면0
,10
,20
처럼10
단위로 숫자가 나온다.
6. iter
, next
함수 활용하기
iter
는 객체의__iter__
메서드를 호출해 주고,next
는 객체의__next__
메서드를 호출해 준다.
- 그럼
range(3)
에iter
에next
를 사용해 보자.
- 반복 가능한 객체에서
__iter__
를 호출하고 이터레이터에서__next__
메서드를 호출한 것과 똑같다. - 즉,
iter
는 반복 가능한 객체에서 이터레이터를 반환하고,next
는 이터레이터에서 값을 차례대로 꺼낸다.
1) iter
iter
는 반복을 끝낼 값을 지정하면 특정 값이 나올 때 반복을 끝낸다.- 이 경우에는 반복 가능한 객체 대신 호출 가능한 객체를 넣어준다.
- 참고로 반복을 끝낼 값은 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) # StopIteration
next(it)
로 숫자를 계속 만들다가2
가 나오면StopIteration
이 발생한다.- 물론 숫자가 무작위로 생성되므로
next(it)
를 호출하는 횟수도 매번 달라진다.
- 다음과 같이
for
반복문에 넣어서 사용할 수도 있다.
>>> 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
조건문으로 매번 숫자가2
인지 검사하지 않아도 되므로 코드가 좀 더 간단해진다.
2) next
next
는 기본값을 지정할 수 있다.- 기본값을 지정하면 반복이 끝나더라도
StopIteration
이 발생하지 않고 기본값을 출력한다. - 즉, 반복할 수 있을 때는 해당 값을 출력하고, 반복이 끝났을 때는 기본값을 출력한다.
- 다음은
range(3)
으로0
,1
,2
세 번 반복하는데next
에 기본값으로10
을 지정했다.
>>> it = iter(range(3))
>>> next(it, 10)
0
>>> next(it, 10)
1
>>> next(it, 10)
2
>>> next(it, 10)
10
>>> next(it, 10)
10
0
,1
,2
까지 나온 뒤에도next(it, 10)
을 호출하면 예외가 발생하지 않고 계속10
이 나온다.- 이터레이터를 만들 때
__iter__
,__next__
메서드 또는__getitem__
메서드를 구현해야 한다는 점만 기억하면 된다.