iterators

Ju Seol·2021년 6월 10일
0

반복가능한 객체

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

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

반복 가능한 객체는 말 그대로 반복할 수 있는 객체인데 우리가 흔히 사용하는 문자열, 리스트, 딕셔너리, 세트가 반복 가능한 객체입니다.

객체가 반복 가능한 객체인지 알아보는 방법은 객체에 __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__를 호출해보면 이터레이터가 나옵니다.

>>> [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 예외를 발생시켜서 반복을 끝냅니다.

for와 반복 가능한 객체

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

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

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

이더레이터 만들기

class 이터레이터이름:
    def __iter__(self):
        코드
 
    def __next__(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())처럼 언패킹으로 변수 여러 개에 값을 할당할 수 있습니다.

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


Counter(3)[0]을 출력했을 때 0이 나왔습니다. 마찬가지로 Counter(3)[1]은 1, Counter(3)[2]는 2가 나왔습니다. 그리고 for 반복문에 Counter를 사용했을 때도 1, 2, 3이 나왔습니다.

소스 코드를 잘 보면 __init__ 메서드__getitem__ 메서드만 있는데도 동작이 잘 됩니다. 클래스에서 __getitem__만 구현해도 이터레이터가 되며 __iter__, __next__는 생략해도 됩니다(초기값이 없다면 __init__도 생략 가능).

__getitem__ 메서드를 구현하면 인덱스로 접근할 수 있는 이터레이터가 됩니다. 먼저 __getitem__은 매개변수로 인덱스 index를 받습니다. 그리고 지정된 index가 반복을 끝낼 숫자 self.stop보다 작을 때 index를 반환합니다. index가 self.stop보다 크거나 같으면 IndexError를 발생시킵니다. 즉, Counter(3)과 같이 반복을 끝낼 숫자가 3이면 인덱스는 2까지 지정할 수 있습니다.

참고로 여기서는 반복할 숫자와 인덱스가 같아서 index를 그대로 반환했지만, index와 식을 조합해서 다른 숫자를 만드는 방식으로 활용할 수 있습니다. 예를 들어 index * 10을 반환하면 0, 10, 20처럼 10 단위로 숫자가 나옵니다. 각자 __getitem__ 메서드를 수정해서 다양한 숫자를 만들어보세요.

iter, next 함수 활용하기

iter

iter는 반복을 끝낼 값을 지정하면 특정 값이 나올 때 반복을 끝냅니다. 이 경우에는 반복 가능한 객체 대신 호출 가능한 객체(callable)를 넣어줍니다. 참고로 반복을 끝낼 값은 sentinel이라고 부르는데 감시병이라는 뜻입니다. 즉, 반복을 감시하다가 특정 값이 나오면 반복을 끝낸다고 해서 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이 발생합니다. 물론 숫자가 무작위로 생성되므로 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인지 검사하지 않아도 되므로 코드가 좀 더 간단해집니다. 즉, 다음 코드와 동작이 같습니다.

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

next

next는 기본값을 지정할 수 있습니다. 기본값을 지정하면 반복이 끝나더라도 StopIteration이 발생하지 않고 기본값을 출력합니다. 즉, 반복할 수 있을 때는 해당 값을 출력하고, 반복이 끝났을 때는 기본값을 출력합니다. 다음은 range(3)으로 0, 1, 2 세 번 반복하는데 next에 기본값으로 10을 지정했습니다.

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, 1, 2까지 나온 뒤에도 next(it, 10)을 호출하면 예외가 발생하지 않고 계속 10이 나옵니다.

지금까지 반복 가능한 객체의 개념과 이터레이터를 만드는 방법을 배웠습니다. 여기서는 이터레이터를 만들 때 __iter__, __next__ 메서드 또는 __getitem__ 메서드를 구현해야 한다는 점만 기억하면 됩니다.

문제

딕셔너리도 반복가능한 객체라서 앞서본 __iter__함수__next__함수를 사용할 수 있고 파이썬 기본함수인 iter, next 또한 사용할 수 있습니다. 다음의 간단한 키를 출력하는 딕셔너리에 대한 for 문을 while문으로 구현해 보세요.

D = {'a':1, 'b':2, 'c':3}
for key in D.keys():
    print(key)

답변

new_d = iter(D)

while True: 
    try:
        X = next(new_d)
    except StopIteration:
        break
    print(X)
        

출처 : 링크텍스트

profile
Hello!

0개의 댓글