Iterator, Generator

Sang Jun Lee·2020년 6월 29일
0

iterator

이터레이터 우리말로는 반복자라고 합니다. for문을 작성할 때 자주쓰는 range를 예를 들면 범위를 지정하면 연속된 모든 숫자를 만들어 내는 것이 아닌 순서대로 값을 꺼내오는 이터레이터를 하나 만들어 낸다고 생각하면 됩니다. iterator는 iterable한 객체를 내장함수 또는 iterable객체의 메소드로 객체를 생성할 수 있습니다.

iterable한 타입으로는 list, dict, set, str, bytes, tuple, range등이 있습니다.

>>> a= [1,2,3]
>>> a_iter = iter(a)
>>> type(a_iter)
<class 'list_iterator'>

파이썬의 내장함수 iter()로 iterator객체를 만들보았습니다. 이렇게 값을 차례로 꺼낼 수 있는 이터레이터 객체는 내장함수인 next()로 첫번째, 두번째 이렇게 값을 출력합니다. 그리고 더 이상 가져올 게 없으면 StopIteration 예외를 발생시켜 반복을 끝냅니다.

>>> a= [1,2,3]
>>> a_iter = iter(a)
>>> type(a_iter)
<class 'list_iterator'>
>>> next(a_iter)
1
>>> next(a_iter)
2
>>> next(a_iter)
3
>>> next(a_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 

iterable한 객체와 iterator객체는 다른 의미인것 즉 반복가능한 객체를 iter()를 통해 iterator로 만들어 주고 next()를 통해 값을 가져오게 됩니다.

클래스에 iter()와 next()를 모두 구현하면 이터레이터를 만들 수 있고 이 객체를 이터레이터 프로토콜(iterator protocol)을 지원한다고 합니다. 이터레이터 프로토콜에 대한 설명은 해당 사이트로 대체하겠습니다. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Iteration_protocols
계속해서 이터레이터를 직접 만들며 설명해보겠습니다.

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         # 현재 숫자 1 증가 
            return r                  # 숫자 반환
        else:                         
            raise StopIteration       # 현재 숫자가 끝낼 숫자와 크거나 같을때 예외발생

for i in Counter(5):
    print(i, end=' ')                 # 결과값은 0, 1, 2, 3, 4
    

Generator

제너레이터는 이터레이터를 만들어주는 함수입니다. 이터레이터는 클래스에 __iter__와 __next__ 매서드를 구현해야 하지만 제너레이터는 yield 라는 키워드만 사용하면 되어 간단하게 작성이 가능합니다.

def number_generator():
    yield 0
    yield 1
    yield 2

for i in number_generator():
    print(i)                 

결과값 아래와 같습니다.
0
1
2

제너레이터 함수를 확인하면 __next__매서드도 있는걸 볼 수 입니다. 이터레이터는 __next__매서드안에서 직접 return 으로 값을 반환해야 하지만 제너레이터는 yield로 지정한 값이 나오게 됩니다. 함수 끝까지 도달하면 자동으로 StopIteration예외가 발생되는 것도 다른점입니다.

간단한 예를 보며 yield가 어떻게 작동하는지 살펴 보겠습니다. 왜 굳이 제너레이터를 사용하는지도 같이요. lazy evaluation의 의미도 살펴보겠습니다.

def number_generator():
    yield 0
    yield 1
    yield 2

g = number_generator()

a= next(g)
print(a)

b= next(g)
print(b)

c= next(g)
print(c)

결과값은
0
1
2

아래 그림에서 보는 것처럼 제너레이터의 첫 yield가 실행하고 함수 바깥으로 값을 전달하고 실행을 양보하는 것을 볼 수 있습니다. 이렇게 yield가 함수 안에서 실행이 되고 값을 전달하고 다음 실행을 양보하는 것을 lazy evalution이라고 하고시간들여 연산하고 메모리 할당하고 사용하지 않을 경우도 있는 경우에 유용하게 사용할 수 있습니다. 라면 10개를 끓여야하고 사람이 한꺼번에 오지 않는 다면 미리 다 끓여 놓을 필요없이 필요할때 마다 끓여 주는게 낭비도 없고 효율적일테니까요. 반대로 대부분의 요소가 확실한 상황이라면 미리 연산하여 준비해두는게 더 효율적일수도 있으니 상황에 맞게 사용하는 게 중요하겠습니다.

한가지 예를 더 보며 좀 더 살펴 보겠습니다.

import time

L = [1,2,3,4,5]
def print_iter(iter):
    for element in iter:
        print(element)

def lazy_return(num):
    print("sleep 1s")
    time.sleep(1)
    return num

print("comprehension_list=")
comprehension_list = [lazy_return(X) for X in L]
print_iter(comprehension_list)
# 리스트 정규식과 제너레이터 정규식의 차이는 [] 와 () 의 차이입니다.
print("generator_exp=")
generator_exp = (lazy_return(x) for x in L)
print_iter(generator_exp)

출력값
comprehension_list=
sleep 1s
sleep 1s
sleep 1s
sleep 1s
sleep 1s               # 먼저 함수안에서 실행을 끝내고 나와 함수밖의 연산을 진행합니다.
1
2
3
4
5
generator_exp=
sleep 1s
1
sleep 1s              # 먼저 함수안에서 하나의 실행 후 빠져나와 함수밖 연산을 진행하고 
2                     # 다시 함수안의 연산을 진행하는 걸 볼 수 있습니다.
sleep 1s
3
sleep 1s
4
sleep 1s
5


profile
Live now and Dream better tomorrow

0개의 댓글