[Python] 제너레이터 함수

2 분 소요

제너레이터

제너레이터(Generator)는 iterator 객체의 한 종류이다. 때문에 제너레이터를 전달하면서 next 함수를 호출하면 값을 하니씩 얻을 수 있다.

제너레이터를 만드는 두가지 함수.

  • 제너레이터 함수(function)
  • 제너레이터 표현식(expression)
>>> def gen_num():  # 제너레이터 함수 정의
        print('first number')
        yield 1   # yield가 하나라도 들어가면 제너레이터가 됨. 첫번재 next 호출에서 이 문장까지 호출.
        print('second number')
        yield 2  # 두번째 next 호출에서 이 문장까지 실행됨.
        print('third number') 
        yield 3  # 세번째 next 호출에서 이 문장까지 실행됨.

>>> gen = gen_num()  # 제너레이터 객체 생성
>>> type(gen)
<class 'generator'>
>>> next(gen)
first number
1
>>> next(gen)
second number
2
>>> next(gen)
third number
3
>>> next(gen)
Trackback (most recent call last):
    File "<pyshell#78>", line 1, in <module>
        next(gen)
StopIteration



제너레이터 장점

>>> def pows(s):
        r = []
        for i in s:
            r.append(i**2)
        return r

>>> st = pows([1,2,3,4,5,6,7,8,9])
>>> for i in st:
        print(i, end=' ')

1 4 9 16 25 36 49 64 81

>>> import sys
>>> sys.getsizeof(st)
100

위 예제는 제너레이터를 사용하지 않는다.

사용한 메모리 공간의 크기를 확인해보면 100 바이트를 사용하고 있다.

>>> def gpows(s):
        for i in s:
            yield i**2

>>> st = gpows([1,2,3,4,5,6,7,8,9])
>>> for i in st:
        print(i, end=' ')

1 4 9 16 25 36 49 64 81

>>> import sys
>>> sys.getsizeof(st)
64

제너레이터를 기반으로 작성하면 메모리 공간을 적게 사용되었다.

제너레이터를 사용하지 않는 경우 메모리 공간의 크기는 리스트의 길이에 비례에서 늘어난다. 하지만 제너레이터를 사용하는 경우 리스트의 길이에 상관없이 사용하는 메모리 공간의 크기가 동일하다. 이유는 단순하다. 제너레이터 객체는 반환할 값들을 미리 만들어서 저장해 두지 않기 때문이다.

생성되는 값들을 순서대로 하나씩 가져다 쓰면 되는 상황에서는 이렇듯 제너레이터를 기반으로 코드를 작성하는 것이 합리적이다.

map과 filter도 사실은 제너레이터 함수이다. 즉 map과 filter 함수가 반환하는 것은 iterator 객체이자 제너레이터 객체이다.

yield from

제너레이터 함수 관련 파이썬 3.3 이상에서 사용할 수 있는 문법 하나가 있다.

>>> def get_nums():
        ns = [0,1,0,1,0,1]
        for i in ns:
            yield i

>>> g = get_nums()
>>> next(g)
0
>>> next(g)
1

위는 0과 1일 순서대로 던져주는 제너레이터이다.

>>> def get_nums():
        ns = [0,1,0,1,0,1]
        yield from ns

>>> g = get_nums()
>>> next(g)
0
>>> next(g)
1

위 코드처럼 다음과 같이 간단히 쓸 수 있다.

제너레이터 표현식

제너레이터 표현식은 제너레이터 함수와 마찬가지로 제너레이터 객체를 생성하는 방법이다. 제너레이터 표현식의 문법 구성이 리스트 컴프리헨션과 거의 똑같다.

>>> def show_all(s): # iterable 객체를 전달하는 이 함수 호출
        for i in s:
            print(i, end=' ')

>>> def times2(): # 제너레이터 함수의 정의
        for i in range(1,10):
            yield 2*i

>>> g = times2()
>>> show_all(g)
2 4 6 8 10 12 14 16 18
>>> g = (2*i for i in range(1,10))
>>> show_all(g)
2 4 6 8 10 12 14 16 18

제너레이터 함수를 별도로 만들면 메모리를 효율적으로 사용할 수 있지만 함수를 별도로 정의해야 한다. 이 상황에서 제너레이터 표현식을 사용하면 간결함과 메모리의 효율성을 모두 잡을 수 있다.

st= [2 * i for i in range(1,10)] # 리스트 컴프리헨션 g = (2 * i for i in range(1,10)) # 제너레이터 표현식

제너레이터 표현식은 괄호만[…]에서 (…)로 바뀌었을 뿐 그 안을 채우는 방법은 리스트 컴프리헨션과 똑같다. 하지만 제너레이터 표현식은 제너레이터 객체의 생성으로 이어진다.

>>> def two():
        print('two')
        return 2

>>> g = (tow() * i for i in range(1,10))
>>> next(g)
two
2
>>> next(g)
two
4



제너레이터 표현식을 직접 전달하기

>>> def show_all(s): # iterable 객체를 전달하는 이 함수 호출
        for i in s:
            print(i, end=' ')

>>> show_all((2 * i for i in range(1,10)))
2 4 6 8 10 12 14 16 18
>>> show_all(2 * i for i in range(1,10))
2 4 6 8 10 12 14 16 18

다음과 같이 함수 호출 시 제너레이터 표현식을 인자로 전달할 수도 있다. 이 경우 소괄호를 생략할 수 있다.