Python

이터레이터(반복자)

haventmetyou 2023. 10. 24. 12:16

이터레이터(iterator)는 값을 차례대로 꺼낼 수 있는 객체

파이썬에서는 이터레이터만 생성하고 값이 필요한 시점이 되었을 때 값을 만드는 방식 사용

> 데이터 생성을 뒤로 미루는 것(지연 평가)

 

반복 가능한 객체 알아보기

> 문자열, 리스트, 딕셔너리, 세트

객체에 __iter__ 메서드가 들어 있는지 확인, dir 함수로 확인 가능

>>> dir( [1, 2, 3] )
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
'__getstate__', '__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 0x0000029200719900>

리스트의 이터레이터를 변수에 저장한 뒤 __next__ 메서드를 호출하면 요소를 차례대로 꺼낼 수 있음

>>> it = [1, 2, 3,].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

요소를 계속 꺼내다가 꺼낼 요소가 없으면 StopIteration 예외 발생시켜 반복 끝냄

>>> # 문자열
>>> it = 'hello, world'.__iter__()
>>> it.__next__()
'h'
>>> it.__next__()
'e'
>>> it.__next__()
'l'
>>> it.__next__()
'l'
>>> it.__next__()
'o'
>>> # 딕셔너리
>>> it = {'a':1, 'b':2}.__iter__()
>>> it.__next__()
'a'
>>> it.__next__()
'b'
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> # range
>>> it = range(3).__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 

for와 반복 가능한 객체

반복 가능한 객체는 요소를 한 번에 하나씩 가져올 수 있는 객체

이터레이터는 __next__ 메서드를 사용해 차례대로 값을 꺼낼 수 있는 객체

둘은 별개의 객체이므로 구분해야 함

반복 가능한 객체에서 __iter__로 이터레이터를 얻음

 

이터레이터 만들기

1. 객체.__iter__() 이터레이터 생성

2. __next__() 다음 요소로 이동

# 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           
            return r                  
        else:        
            raise StopIteration
        
for i in Counter(3):
    print(i, end=' ')
    
# 실행 결과
0 1 2

 

이터레이터 언패킹

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

이터레이터 언패킹 가능, Counter()의 결과를 변수 여러 개에 할당 가능

이터레이터가 반복하는 횟수와 변수의 개수는 같아야 함

map도 이터레이터이므로 a, b, c = map(int, input().split())처럼 언패킹으로 변수 여러 개에 값을 할당할 수 있음

 

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

>>> _, b = [0, 1]
>>> b
1

>>> for _ in [0, 1, 2]:
...     print('hello')
...
hello
hello
hello

a, b = range(2)와 같음

반환값을 언패킹했을 때 _에 할당하는 것은 특정 순서의 반환값 사용하지 않고 무시하겠다는 표현

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

위와 같은 코드는 언패킹했을 때 두 번째 변수는 사용하지 않겠다는 뜻

 

인덱스로 접근할 수 있는 이터레이터 만들기
# 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

 

iter, next 함수 활용
>>> it = iter(range(3))
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

 

iter

iter는 반복을 끝낼 값을 지정하면 특정 값이 나올 때 반복을 끝냄

이 경우, 반복 가능한 객체 대신 호출 가능한 객체(callable) 넣어 줌

반복을 끝낼 값은 sentinel이라고 부르는데, 감시병이라는 뜻 = 반복을 감시하다 특정 값이 나오면 반복을 끝냄

ex) random.randint(0, 5)와 같이 0부터 5까지 무작위로 숫자를 생성할 때 2가 나오면 반복을 끝내도록 만들 수 있음

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

>>> # iter(호출가능한객체, 반복을끝낼값)
>>> import random
>>> it = iter(lambda: random.randint(0, 5), 2)
>>> next(it)
3
>>> next(it)
4
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

for 반복문에 넣어 사용할 수 있음

>>> import random
>>> for i in iter(lambda : random.randint(0, 5), 2):
...     print(i, end=' ')
...
1 4 1 >>>

iter 함수를 활용하면 if 조건문으로 매번 숫자가 2인지 검사하지 않아도 되므로 코드가 간단해짐

다음 코드와 동작이 같음

>>> import random
>>>
>>> while True:
...     i = random.randint(0, 5)
...     if i == 2:
...         break
...     print(i, end=' ')
...
3 4 4 4 5 0 1 1 4 5 1 1 1 1 >>>

 

next

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

반복할 수 있을 때는 해당 값 출력, 반복이 끝났을 때는 기본값 출력

>>> # next(반복가능한객체, 기본값)
>>> it = iter(range(3))
>>> next(it, 'cat')
0
>>> next(it, 'cat')
1
>>> next(it, 'cat')
2
>>> next(it, 'cat')
'cat'
>>> next(it, 'cat')
'cat'

예외가 발생하지 않고 cat 출력

 

연습 문제 예제
class MultipleIterator:
    def __init__(self, stop, multiple):
        self.stop = stop
        self.multiple = multiple
        self.current = 0
    
    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