DEV Community

Cover image for [ Python ] - Iterator & Generator
Minho Lee
Minho Lee

Posted on • Edited on

[ Python ] - Iterator & Generator

최근에 Iterable, Iterator, Generator에 대해 공부를 했는데,
도대체 어떻게 정리를 하고 글을 써야 할지 몰라서 거의 일주일은 미룬 것 같다.

그런데 이렇게 미루다 간 평생 못쓸 것 같아 일단 두서없이 쓰더라고 일단 그냥 써보려고 한다.

쓰다 보면 내가 어느 부분을 잘 알고 어느 부분을 잘 모르는지 알 거라 생각했다.


Iterable : 반복이 가능한 모든 객체를 의미한다.
쉽게 말해, for in 구문에서 in 뒤에 올 수 있는 모든 값은 Iterable 하다고 할 수 있다.

Iterator : 요소가 여러 개인 데이터 타입에서 각 요소를 하나씩 꺼내어 어떠한 처리를 수행할 수 있도록 도와주는 객체이다.

Generator : Iterator의 한 종류이다. 하나의 요소를 꺼내려고 할 때마다 요소 Generator를 수행한다. Python에서는 주로 yield 문으로 구현한다.

Generator는 모든 데이터를 가지고 와서 사용하지 않는다. 그때그때 yield에 의해 하나씩 가져와서 사용한다. 그래서 메모리의 효율성이 좋다는 것이다.

뭔 소리인가? 일단 코드부터 살펴보자.


메모리 크기 비교

하나는 List Comprehension을 이용해 10만의 크기를 가진 리스트를 생성, 남은 하나는 Generator 객체를 만들 것이다.

size.py

import sys

# Function Version
my_list = [i for i in range(100000)]
print(type(my_list), sys.getsizeof(my_list), "bytes")

# Generator Version
my_gen = (i for i in range(100000))
print(type(my_gen), sys.getsizeof(my_gen), "bytes")
Enter fullscreen mode Exit fullscreen mode

Result

<class 'list'> 800984 bytes
<class 'generator'> 112 bytes
Enter fullscreen mode Exit fullscreen mode

크기가 1일 땐, 88과 112로 my_list가 더 작다.
하지만 크기가 커질수록 점점 차이가 나며, 위처럼 크기가 10만이라면 my_list의 크기는 800984 bytes가 나오게 된다.

my_gen의 크기는 크기가 몇이든 항상 112 bytes이다.
10만의 크기를 기준으로 약 8,000배의 차이가 난다.

위에 Generator는 모든 데이터를 가지고 오지 않는다고 했는데, 위 예제를 한 번 더 보면 my_list라는 변수에는 크기가 10만 개인 리스트가 메모리에 올라가 있다.

하지만 my_gen이라는 변수는 Generator 객체이다. 이미 10만 개의 크기를 갖고 있는 것이 아니라 그때그때 하나씩 가져와서 쓰는 친구이다.

위에서도 언급했지만, Generator는 Iterator의 한 종류이고, Iterator는 요소를 하나씩 꺼내어 어떠한 작업을 수행할 수 있도록 도와준다.

그리고 예를 들어 일반적인 for 문은 모든 요소를 다 돌아야지만 작업이 끝나고 다른 작업을 할 수 있는데, Generator는 한 번 yield로 반환 후, 다른 작업을 할 수 있다.


속도 비교

10만 개의 크기를 가진 피보나치수열을 리스트로 생성 후, 하나씩 총 10만 번을 출력하는 코드를 작성하려고 한다.

  1. 하나는 일반 함수로 10만 개의 크기를 가진 피보나치수열을 리스트로 생성할 것이고 for 문을 통해 출력한다.
  2. 하나는 Generator를 이용할 것이다.
  3. 모든 출력은 pass로 대체한다.

time.py

import time

# Function Version
start_func = time.time()


def fibonacci(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result


for _ in fibonacci(100000):
    pass

end_func = time.time()
print('Function time = ', end_func - start_func)


# Generator Version
start_gen = time.time()


def fibonacci(n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b


for _ in fibonacci(100000):
    pass

end_gen = time.time()
print('Generator time = ', end_gen - start_gen)
Enter fullscreen mode Exit fullscreen mode

Result

Function time =  0.3048691749572754
Generator time =  0.13507723808288574
Enter fullscreen mode Exit fullscreen mode

위 예제를 통해 알 수 있듯이, 속도에 있어서 큰 차이를 보인다. 100만 개의 크기도 테스트를 해봤는데, 크기가 클수록 더 차이게 크게 난다.

왜 속도의 차이가 날까?

일반적인 방법으로는 하나하나 리스트에 담고 마지막에 for 문을 돌려가며 출력을 한다.

하지만 Generator를 이용하면 list에 담지 않아도 된다.
굳이 어느 메모리에 리스트를 올려두지 않아도 Generator가 하나하나씩 가져와서 출력을 하기 때문이다.

정리

Generator를 이용하면 메모리를 효율적으로 사용할 수 있다.
물론 항상 그런 것은 아니다.

그리고 DB Session과 같이 많이 반복되거나 끝이 없는 작업을 할 때 Generator를 사용하면 매우 좋을 것 같다.

아직 많이 부족하다. 하지만 항상 생각하고 더 좋은 코드를 짜려고 노력해야 더 좋은 개발자가 될 수 있다.

Top comments (0)