3 minute read

본 포스팅은 “윤성우의 열혈 파이썬 중급편” 책 내용을 기반으로 작성되었습니다. 잘못된 내용이 있을 경우 지적해 주시면 감사드리겠습니다.

8-1. 제너레이터에 대한 이해와 제너레이터 함수

전 시간에 iterator 객체에 대해 공부하였다.
제너레이터는 iterator 객체의 한 종류이다. 그러므로, 제너레이터를 전달하고 next 함수를 호출하면 값을 하나씩 얻을 수 있다.
제너레이터를 만드는 방법은 크게 두가지가 있다.
1) 제너레이터 함수 → 제너레이터를 만들기 위한 함수 정의
2) 제너레이터 표현식 → 제너레이터를 만들기 위한 식

먼저 함수 기반 제너레이터를 만들어보자.

def grt_num():
    print('One')
    yield 1
    print('Two')
    yield 2
    print('Three')
    yield 3

grt = grt_num()
type(grt)
next(grt)
next(grt)
next(grt)
next(grt)
(결과) <class 'generator'>
     One
     1
     Two
     2
     Three
     3
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
    StopIteration

grt_num() 함수를 보면 yield가 보인다.
함수 안에 yield가 보인다? 그럼 제너레이터 함수다! 라고 보면 된다. 보다시피 next 함수를 호출 하였더니 print문과 더불어 yield 부분도 출력되었다. yieldreturn의 역할을 하게 되는 것이다! 한번 더 출력하면 StopIteration 예외가 발생한다. 이를 보면 제너레이터 객체는 iterator 객체임에 틀림 없어 보인다.

8-2 제너레이터가 갖는 장점

두 코드를 비교해보자.

코드1

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

st = square([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

코드2

def gsquare(s):
    for i in s:
        yield i ** 2

gst = gsquare([1, 2, 3, 4, 5, 6, 7, 8, 9])
for i in gst:
    print(i, end = ' ')
(결과) 1 4 9 16 25 36 49 64 81

코드1과 코드2는 같은 결과를 출력한다. 제너레이터 함수 사용 유무만 다를 뿐이다. 과연 두 함수가 각각 차지하는 메모리 공간의 크기는 어떨까?

import sys
sys.getsizeof(st)
sys.getsizeof(gst)
(결과) 192
     120

제너레이터 함수를 사용할 때 메모리 공간 크기가 더 적게 들어간다. 위 예제 코드들이 차지하는 메모리 공간 크기는 얼마 차이가 안나는 것처럼 보인다. 그러나 제너레이터 객체는 반환할 값들을 미리 만들어서 저장하지 않기 때문에 리스트 길이에 상관없이 사용하는 메모리 공간 크기는 동일하다!

앞서 배운 map과 filter 함수는 iterator 객체를 반환한다고 했다. 하지만 사실 이들은 iterator 객체이자 제너레이터 객체라고 하니 참고하자!

8-3. yield from

위 코드2에서 제너레이터 함수를 구현하기 위해 for문을 사용하였다. 이걸 더 줄일 수 없을까?

def gsquare(s):
    yield from s ** 2

gst = gsquare([1, 2, 3, 4, 5, 6, 7, 8, 9])
for i in gst:
    print(i, end = ' ')
(결과) Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 2, in gsquare
     TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'int'

혹시 될 거라고 생각했는가? 미안하다. 나는 될 줄 알았다. 다음과 같이 **을 포함한 사칙연산은 적용이 안된다. 그러나 yield from 기반의 제너레이터 함수를 사용하여 리스트 자체를 for문 없이 불러오는건 가능하다!

def glist(s):
    yield from s

gst = glist([1, 2, 3, 4, 5, 6, 7, 8, 9])
for i in gst:
    print(i, end = ' ')
(결과) 1 2 3 4 5 6 7 8 9

8-4. 하나의 문장으로 제너레이터를 구성하는 방법

제너레이터 함수를 사용하여 2의 배수를 생성해보자.

def show_two_times(s):
    for i in s:
        print(i, end = ' ')

def two_times():
    for i in range(1, 10):
        yield i * 2

g = two_times()
show_two_times(g)
(결과) 2 4 6 8 10 12 14 16 18

생성은 됐는데 함수를 2개나 쓰게 된다.

앞서 제너레이터 함수와 더불어 제너레이터 표현식에 대해서도 언급했는데, 제너레이터 표현식 이란 녀석을 써보자!

def show_two_times(s):
    for i in s:
        print(i, end = ' ')

g = (2 * i for i in range(1, 10))
show_two_times(g)
(결과) 2 4 6 8 10 12 14 16 18

리스트 컴프리헨션 아닌가요? 할 수도 있다. 리스트 컴프리헨션은 []이고 제너레이터 표현식은 ()을 쓴다! 제너레이터 표현식을 활용하면 제너레이터 객체가 생성된다.

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

다음과 같이 저장할 변수 없이도 활용 가능하다.

def show_two_times(s):
    for i in s:
        print(i, end = ' ')

show_two_times((2 * i for i in range(1, 10)))

Leave a comment