본문 바로가기
Programming/Python

이터레이터? 제너레이터?

by DUSTIN KANG 2023. 11. 28.

이터레이터(Iterator)와 제너레이터(Generator)는 알고도 가끔 헷갈릴 때가 있습니다.

먼저, 결론부터 말하자면 다음과 같습니다.

  • iterable 객체 : `iter` 함수에 인자로 전달이 가능한 반복 가능한 객체 ex. List, Dict, Set, String, range()
  • iterator(이터레이터) : 그 반복 가능한 객체를 `iter` 함수에 인자로 전달해 순회할 수 있는 객체
  • generator(제너레이터) : Iterator 객체를 만들 수 있는 함수 (메모리 절약!)

어떤 의미인지는 간략하게 알았으니 코드를 보면서 자세히 확인해봅시다.


🌱 iterable & Iterator

앞서 나왔듯, iterable한 객체는 iter가 가능(able)한 객체를 말합니다.

대표적인 iterable한 타입으로 list, dict, set, bytes, tuple, range, enumerate 등이 있습니다. 

iterable객체는 공통적으로 `__iter__`라는 매직 메소드를 가지고 있습니다.  이 때문에 iterator() 함수를 인자를 씌우면 `next()`라는 함수로 순회할 수 있게되는 것입니다. 

a = [1,2,3] # iterable
'__iter__' in dir(a) # True

b = a.__iter__() # iterator
'__next__' in dir(b) # True
  • `next()` : 이터레이터에서 다음 값을 가져올 수 있는 함수이다.
  • `iter()` : iterable 객체를 iterator로 바꿔주는 함수이다. 바뀐 이터레이터를 next()를 통해 값을 하나씩 가져올 수 있다.
만약, iterator가 iterable한 객체를 모두 순회했다면 `StopIteration`을 발생시킵니다.
다시 객체를 순회하고 싶으면 새로운 iterator 객체를 생성하면 됩니다.

 

iterable에 유용한 함수

이터레이블한 객체를 인수로 받아 사용하는 유용한 함수들이 있습니다.

대표적으로, `any` `all` `zip`이 있습니다.

all(iterable) 인수의 모든 원소가 참인 경우 True, 그렇지 않으면 False 리턴
ex. 리스트 내 0의 여부를 확인할 때 사용
any(iterable) 원소 중 하나라도 참이면 True, 모두 거짓일 때만 False
zip(*iterable) iterable한 객체를 인수로 동일한 개수로 이루어진 자료형을 묶어 반환
zip_longest(*iterable, fillvalue=None) zip과 차이라면 원소의 갯수가 다를 경우 적은 쪽에 fillvalue를 채움
itertools.cycle(iterable) 원소들을 계속 반복하고 싶을 때 사용
itertools.permutations(iterable, r=None) 서로 다른 N개의 원소 중 r개를 중복적으로 선택하는 방법(순열)
itertools.combinations(iterable, r=None) 서로 다른 N개의 원소 중 서로 다른 r개를 중복 없이 선택하는 방법(조합) 

🌱 Generator

 

제너레이터(Generator)는 값을 하나씩 생성하는 파이썬의 강력한 도구입니다. 즉, iterator 객체를 만들 수 있는 함수를 말합니다. 

그럼 제너레이터는 어떻게 표현할 수 있을까요? 다음과 같이 `yield`문이나 표현식을 사용해 만들 수 있습니다.

 

 

제너레이터는 큰 데이터 세트나 연속적인 데이터를 처리할 때 유용합니다.

큰 데이터를 메모리에 전체 저장하지 않고 필요한 만큼만 값을 생성해서 좀 더 효율적인 코드를 작성할 수 있기 때문입니다. 

이로인해, 메모리 최적화에도 도움을 줄 수 있습니다.

 

다음 코드를 봅시다.

첫번째 함수는 제너레이터를 활용해 하나씩 값을 추출헀고, 두번째 함수는 리스트에 모든 값을 저장한다음 반환하는 식으로 코드를 작성했습니다. 이 둘의 메모리 차이를 보면 제너레이터를 사용했을 때 메모리가 적을 것입니다. 이유는 전체 데이터를 저장하지 않았기 때문입니다.

출력해보면 gen2는 리스트에 있는 모든 데이터를 반환하는 반면 gen은 `next(gen)`으로 하나씩 반환할 것입니다.

def gen_fun(n):
    for i in range(n):
        yield i

def gen_fun2(n):
    lst = []
    for i in range(n):
        lst.append(i)
    return lst

gen = gen_fun(100)
gen2 = gen_fun2(100)
print(sys.getsizeof(gen), sys.getsizeof(gen2))

next(gen)

 

아래 코드는 표현식만 다르게 했을 뿐 같은 제너레이터를 사용하고 있습니다.

리스트를 사용하면 크기만큼 메모리에 공간을 할당하게 됩니다. 그러나 next 함수로 호출하게되면 값을 생성하고 해당 값만 메모리에 올라가기 때문에 메모리 절약을 할 수 있습니다. 

import sys
generator = (i for i in range(1, 100)) # 제너레이터 표현식을 작성했을 경우
iterator = [i for i in range(1, 100)]

print(sys.getsizeof(generator), sys.getsizeof(iterator))

 

또다른 방법, 이터레이터 클래스

제너레이터를 통해 이터레이터를 만들 수 있었지만 이터레이터 클래스로 이터레이터를 만들 수 있습니다.

아래 코드를 보면 `self.position`이라는 상태 값을 갖는 것을 확인할 수 있습니다. 이는 다음 값을 생성하기 위해 정보를 저장하는 변수 입니다. 이터레이터를 통해 이터레이터를 생성할 수 있습니다. 그러나, 제너레이터와 같은 가독성 좋고 쉬운 방법이 있는데.. 저는 제너레이터를 사용할 것 입니다.

class MyIterator:
    def __init__(self, data):
        self.data = data  # 데이터
        self.position = 0 # 상태 값 (현재 상태의 추적을 위해)

    def __iter__(self):
        return self # 이터레이터 객체 생성
    
    def __next__(self):
        if self.position >= len(self.data): # 데이터 길이보다 큰 경우 예외 발생
            raise StopIteration
        result = self.data[self.position] # 결과
        self.position += 1 # 다음 추가
        return result 
    

items = MyIterator([1,2,3])

next(items)

 

제너레이터 관련 기능들

yield from

제너레이터를 합성해서 사용할 때 쓰이는 명령어입니다. 연속으로 제너레이터를 사용했을 때 합성한 경우와 수동으로 포함한 경우를 비교했을 때 벤치마킹 결과 합성했을 때가 더 빠른 시간 결과를 볼 수 있습니다.

 

send()

제너레이터의 값을 재개(Resume)할 때 사용하는 메서드입니다.

yield 가 끝나고 `send()`를 호출하면 해당 값이 `yield`의 결과 값으로 제너레이터를 진행합니다.

def generator():
    value = yield 1 # 1. 1을 반환 -> 값을 보냄
    print(value) # 3. Hello 출력 
    yield 2

gen = generator()
next(gen)  # 0. 시작
gen.send('Hello')  # 2. Hello 값을 보냄
💡 `send`를 사용할 때, 먼저 `next()`로 제너레이터를 시작한 다음 `send()`를 사용해야 한다.

 

throw()

`throw(exception_type)`은 제너레이터 함수에 예외를 던질 때 사용합니다.

제너레이터 함수에 예외 구문을 내포하여 사용할 수 있습니다. 다음 마지막 코드를 보면 예외 구문이 yield를 감싸는 것으로 볼 수 있습니다. . 물론, 예외를 발생시킬 수 있는 기능이 있지만 가독성 면에서 다시 예외를 발생시키는데 준비하는 코드가 필요하며 함수 또한 깊어지기 때문에 좋지 않다고 합니다.

def my_generator():
    yield 1

    try:
        yield 2
    except MyError:
        print('에러 발생')
    else:
        yield 3
    yield 4

class MyError(Exception):
    pass

it = my_generator()
print(next(it))  # 1을 내놓음
print(next(it))  # 2를 내놓음
print(it.throw(MyError('test error'))) # 에러 발생 4

 


☕️ 포스팅이 도움이 되었던 자료

오늘도 저의 포스트를 읽어주셔서 감사합니다.

설명이 부족하거나 이해하기 어렵거나 잘못된 부분이 있으면 부담없이 댓글로 남겨주시면 감사하겠습니다.