파이썬 | 이터레이터

CHOI·2021년 12월 31일
0

Python

목록 보기
30/33
post-thumbnail

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

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

만약 연속된 숫자를 미리 만들어놓으면 숫자가 적을 때는 상관없지만 아주 많으면 너무 큰 메모리 공간을 낭비하게 된다. 그래서 파이썬에서는 이터레이터를 생성하고 값이 필요한 시점에 값을 만드는 방식을 사용한다. 즉, 데이터 생성을 뒤로 미루는 것인데 이러한 방식을 지연 평가(lazy evaluation)이라고 한다.

참고로 이터레이터는 반복자라고도 하는데 앞으로 아래에서는 이터레이터라고 부르겠다.

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

이처럼 이터레이터는 __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 에 반복 가능한 객체를 사용했을 때 일어나는 과정에 대해서 살펴보자. 다음과 같이 forrange(3) 을 하면 우선 range 에서 __iter__ 로 이터레이터를 얻는다. 그리고 한 번 반복될 때 마다 이터레이터에서 __next__ 로 숫자를 꺼내서 i 에 넣고 지정된 숫자가 3 이 되면 StopIteration 를 발생시켜서 반복을 끝낸다.

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

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

% 시퀀스 객체와 반복 가능한 객체의 차이

앞서 시퀀스 객체를 배울 때 리스트, 튜플, range , 문자열은 시퀀스 객체라고 했는데 이번 단원에서는 반복 가능한 객체라고 했다. 이 둘의 차이점이 무엇일까?

반복 가능한 객체는 시퀀스 객체를 포함한다.

리스트, 튜플, range , 문자열은 반복 가능한 객체이면서 시퀀스 객체이다. 하지만 딕셔너리, 세트는 반복 가능한 객체이지만 시퀀스 객체는 아니다. 왜냐하면 시퀀스 객체는 요소의 순서가 정해져 있고 연속적(sequence)으로 이어져 있어야 하지만 딕셔너리와 세트는 요소(키)의 순서가 정해져 있지 않기 때문이다. 따라서 시퀀스 객체가 반복 가능한 객체보다 좁은 개념이다. (시퀀스 객체 < 반복 가능한 객체)

즉, 요소의 순서가 정해져 있고 연속적으로 이어져 있으면 시퀀스 객체, 요소의 순서와 상관없이 한 번에 하나씩 꺼낼 수 있으면 반복 가능한 객체이다.

2. 이터레이터 만들기

이제 __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 는 사용할 필요가 없을 때 반환값을 사용하지 않고 무시하겠다는 관례적 표현으로 _ 에 할당한다.

3. 인덱스 활용 ( getitem )

지금까지는 __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 를 받는데 indexself.stop 보다 작으면 index 를 리턴하고 아니면 IndexError 예외를 발생시킨다.

def __getitem__(self, index):    # 인덱스를 받음
        if index < self.stop:        # 인덱스가 반복을 끝낼 숫자보다 작을 때
            return index             # 인덱스를 반환
        else:                        # 인덱스가 반복을 끝낼 숫자보다 크거나 같을 때
            raise IndexError         # 예외 발생

이렇게 하면 Counter(3)[0] 처럼 인덱스로 이터레이터에 접근할 수 있다.

4. iter , next 활용

이번에는 파이썬 내장 함수 iternext 에 대해서 알아보자. 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

iter 는 반복을 끝낼 값을 지정하면 지정한 값이 나올 때 반복을 끝낸다. 이 경우에는 반복 가능한 객체 대신 호출 가능한 객체(callable) 를 넣어준다. 참고로 반복을 끝낼 값을 sentinel 이라고 하는데 감시병이라는 뜻이다. 즉, 반복을 감시하다가 특정 값이 나오면 반복을 끝낸다.

  • iter(호출가능한객체, 반복을끝낼값)

예를 들어서 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

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__ 메서드를 구현해야 한다는 점을 기억하자.

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글