본문 바로가기
BASIC

이터레이터

by 자동매매 2023. 11. 28.

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

데이터 생성을 뒤로 미루는 것인데 이런 방식을 지연 평가(lazy evaluation)라고 합니다.

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

1. 반복 가능한 객체 (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']

 

 

ㄱ) 리스트에서 __iter__를 호출해보면 이터레이터 반환

>>> [1, 2, 3].__iter__()
<list_iterator object at 0x03616630>

 

ㄴ) 리스트의 이터레이터를 변수에 저장한 뒤 __next__ 메서드를 호출해보면 요소를 차례대로 꺼낼 수 있습니다.

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

>>> 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

 

a = "Hello, world!"
it = iter(a)             # it = a.__iter__()
for i in range(len(a)):
    print(next(it))      # print(it.__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(3).__iter__()
<range_iterator object at 0x0000023D95B31D70>

 

for와 반복 가능한 객체

 

반복 가능한 객체는 요소를 한 번에 하나씩 가져올 수 있는 객체이고, 이터레이터는 __next__ 메서드를 사용해서 차례대로 값을 꺼낼 수 있는 객체입니다.

반복 가능한 객체(iterable)와 이터레이터(iterator)는 별개의 객체이므로 둘은 구분해야 합니다.

즉, 반복 가능한 객체에서 __iter__ 메서드로 이터레이터를 얻습니다.

 
 

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

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

 

 

2. 이터레이터 만들기

이제 __iter__, __next__ 메서드를 구현해서 직접 이터레이터를 만들어보겠습니다. 간단하게 range(횟수)처럼 동작하는 이터레이터입니다.

class 이터레이터이름:
    def __iter__(self):
        코드
 
    def __next__(self):
        코드
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

 

    def __iter__(self):
        return self         # 현재 인스턴스를 반환

 

이터레이터 언패킹

참고로 이터레이터는 언패킹(unpacking)이 가능합니다. 즉, 다음과 같이 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())처럼 언패킹으로 변수 여러 개에 값을 할당할 수 있습니다.

 
 
반환값을 _에 저장하는 이유

함수를 호출한 뒤 반환값을 저장할 때 _(밑줄 문자)를 사용하는 경우가 있습니다.

>>> _, b = range(2)
>>> b
1
>>> a, _, c, d =  range(4)
>>> a, c, d
(0, 2, 3)

 

인덱스로 접근할 수 있는 이터레이터 만들기

지금까지 __iter__ __next__ 메서드를 구현하는 방식으로 이터레이터를 만들었습니다.

이번에는 __getitem__ 메서드를 구현하여 인덱스로 접근할 수 있는 이터레이터를 만들어보겠습니다.

 

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

 

3. iter, next 함수 활용하기

1) iter

iter는 반복을 끝낼 값(감시변, sentinel)을 지정하면 특정 값이 나올 때 반복을 끝냅니다. 

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

이때 호출 가능한 객체를 넣어야 하므로 매개변수가 없는 함수 또는 람다 표현식으로 만들어줍니다.

>>> 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

 

>>> 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 

2) next

next는 기본값을 지정할 수 있습니다. 기본값을 지정하면 반복이 끝나더라도 StopIteration이 발생하지 않고 기본값을 출력합니다. 

  • next(반복가능한객체, 기본값)
>>> 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부터 지정된 숫자보다 작을 때까지 만들어야 합니다.

class MultipleIterator:
    def __init__(self, stop, multiple):
        self.stop = stop
        self.current = 0
        self.multiple = multiple

    def __iter__(self):
        return self

    def __next__(self):
        self.current += 1
        if self.current * self.multiple < self.stop:
            return self.current * self.multiple
        else:
            raise StopIteration


for i in MultipleIterator(20, 3):
    print(i, end=" ")

print()
for i in MultipleIterator(30, 5):
    print(i, end=" ")

 

3 6 9 12 15 18 
5 10 15 20 25 

 

[ 연습문제 ]

표준 입력으로 정수 세 개가 입력됩니다(첫 번째 정수는 시작하는 초, 두 번째 정수는 반복을 끝낼 초, 세 번째 정수는 인덱스이며 입력되는 초의 범위는 0~100000, 입력되는 인덱스의 범위는 0~10입니다). 다음 소스 코드에서 시간 값을 생성하는 이터레이터를 만드세요.

  • 시간 값은 문자열이고 시:분:초 형식입니다. 만약 숫자가 한 자리일 경우 앞에 0을 붙입니다(예: 12:01:08).
  • 1초는 00:00:01입니다. 23:59:59를 넘길 경우 00:00:00부터 다시 시작해야 합니다.
  • 시간은 반복을 끝낼 초 직전까지만 출력해야 합니다(반복을 끝낼 초는 포함되지 않음).
class TimeIterator:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.current = self.start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current<self.end:
            result = self.express(self.current)
            self.current += 1
            return result
        else:
            raise StopIteration

    def __getitem__(self, index):
        if index < self.end - self.start:
            result = self.start + index
            result = self.express(result)
            return result

        else:
            raise IndexError

    def express(self, value):
        min, sec = divmod(value, 60)
        hour, min = divmod(min, 60)
        day, hour = divmod(hour, 24)
        r = f"{hour:02d}:{min:02d}:{sec:02d}"
        return r


start, stop, index = map(int, input().split())

for i in TimeIterator(start, stop):
    print(i)

print("\n", TimeIterator(start, stop)[index], sep="")

 

[입력]
0 3 2

[결과]
00:00:00
00:00:01
00:00:02

00:00:02

[입력]
88234 88237 1

[결과]
00:30:34
00:30:35
00:30:36

00:30:35

 

'BASIC' 카테고리의 다른 글

코루틴  (0) 2023.11.28
제너레이터  (0) 2023.11.28
회문 판별과 N-gram 만들기  (1) 2023.11.28
파일 다루기  (2) 2023.11.28
set  (0) 2023.11.27

댓글