파이썬

파이썬(python) - 제너레이터(Generator)

티베트 모래여우 2020. 12. 28. 20:36
반응형

제너레이터(Generator)란?

제너레이터는 쉽게 설명해서 *이터레이터(Iterator)를 생성하는 객체라고 할 수 있습니다. 즉 모든 제너레이터는 이터레이터에 속합니다. 좀 특별한 이터레이터 정도로 이해하셔도 될 것 같네요.

컴프리헨션(Comprehension) 문법 혹은 함수 내부에 yield 키워드를 사용함으로써 만들 수 있습니다.

가장 큰 특징으로는 lazy하다는 점이 있는데 이는 포스팅 후반에 다시 다뤄보도록 하겠습니다.

(* 이터레이터(Iterator)와 이터러블(Iterable), 컴프리헨션(Comprehension)에 대한 내용은 이전 포스팅을 참조해주세요)

 

파이썬(python) - 이터레이터(Iterator)

이터레이터(Iterator)란? 이터레이터는 순서대로 다음 값을 리턴할 수 있는 객체를 의미합니다. 자체적으로 내장하고 있는 next 메소드를 통해 다음 값을 가져올 수 있습니다. 여기서 '순서대로'라

tibetsandfox.tistory.com

 

 

 

파이썬(python) - 컴프리헨션(Comprehension) 문법

컴프리헨션(Comprehension) 문법이란? 컴프리헨션은 파이썬의 자료구조(list, dictionary, set)에 데이터를 좀 더 쉽고 간결하게 담기 위한 문법입니다. 여기서 말하는 '쉽고 간결하게' 데이터를 담는 방법

tibetsandfox.tistory.com


제너레이터 만들기

앞서 컴프리헨션 문법과 yield 키워드를 통해 제너레이터를 생성할 수 있다고 언급했었습니다.

우선 컴프리헨션 문법으로 만들어봅시다.

foo = (i for i in range(1, 6))
print(type(foo)) # <class 'generator'>

이렇게 컴프리헨션 문법을 통해 생성된 제너레이터를 제너레이터 표현식이라고 칭합니다.

[]가 아니라 ()를 사용했음에 주의해주세요.

위의 제너레이터 표현식으로 생성된 제너레이터를 yield 키워드를 이용해 만들어보겠습니다.

def func():
    for i in range(1, 6):
        yield i

foo2 = func()
print(type(foo2)) # <class 'generator'>

함수 안에 yield 키워드를 사용하면 그 함수는 무조건 제너레이터가 됩니다. (설령 yield 키워드가 함수 내에서 전혀 사용되지 않는다 하더라도 제너레이터로 만들어버립니다.)

이렇게 생성된 foo와 foo2 모두 next 메소드를 통해 값에 접근할 수 있습니다.

또한 제너레이터는 이터레이터에 속하기 때문에 불러올 값이 더 이상 없으면 StopIteration 예외를 발생시킵니다.


yield 키워드

yield 키워드는 그럼 어떻게 동작하는걸까요?

언뜻 보기엔 return과 비슷하게 동작하는 것 같지만 큰 차이가 있습니다.

예제와 함께 알아봅시다.

def func():
    yield 1
    yield 2
    yield 3

foo = func()

print(next(foo)) # 1
print(next(foo)) # 2
print(next(foo)) # 3

제너레이터 함수는 yield를 통해 값을 리턴합니다.

next메소드나 for문 순회 등을 통해 저 값들을 리턴받을 수 있는데

중요한 점은 yield가 호출된다고 해서 함수가 종료되는 것이 아니라는 것 입니다.

yield는 호출되면 값을 반환하고 그 시점에서 함수를 잠시 정지시킵니다. 그리고 다음 next가 호출되면 정지된 시점부터 다시 로직을 실행합니다.

return은 호출되면 함수를 종료하죠? yield와 return의 차이가 바로 이것입니다.

첨언하자면, 제너레이터 내부에서 return을 사용 시 현재 값에 상관없이 무조건 StopIteration 예외를 발생시킵니다.

 

def func():
    print("1 리턴 전")
    yield 1
    print("1 리턴 후, 2 리턴 전")
    yield 2
    print("2 리턴 후, 3 리턴 전")
    yield 3
    print("3 리턴 후")


foo = func()

print(next(foo))
print(next(foo))
print(next(foo))

이해를 돕기 위해 각 yield 사이에 print문을 넣어보았습니다.

출력 결과는 다음과 같습니다.

1 리턴 전
1
1 리턴 후, 2 리턴 전
2
2 리턴 후, 3 리턴 전
3

마치 각 yield를 체크포인트로 두고 로직을 실행하는 것 처럼 보입니다.

이렇게 제너레이터 내에 다양한 로직을 추가할 수 있다는 점에서 yield 키워드는 상당히 유용한 키워드입니다.

제너레이터의 체크포인트


yield from

yield로 iterable한 객체의 요소를 하나씩 리턴해 줄 수도 있습니다.

이 경우 yield에 더해 from이라는 키워드를 사용하면 됩니다.

def func():
    _list = [1, 2, 3, 4, 5]
    for i in _list:
        yield i

즉 위와 같이 리스트를 순회하며 각각 값을 yield로 반환하는 대신,

def func():
    _list = [1, 2, 3, 4, 5]
    yield from _list

이렇게 for문 없이 yield로 반환하는 것도 가능합니다.

사용법은 위의 예제나 아래 예제나 똑같습니다.

def generator(stop_number):
    num = 0
    while True:
        if num >= stop_number:
            return
        num += 2
        yield num


def func(stop_number):
    gen = generator(stop_number)
    yield from gen


foo = func(10)

추가로 이렇게 yield from 키워드로 제너레이터를 전달하는 것도 가능합니다.

위 예제는 10 이하의 짝수를 foo 객체의 next로 하나씩 꺼내올 수 있습니다.


게으른 제너레이터

포스팅의 시작 문단에서 제너레이터는 lazy하다고 말씀드렸습니다. 이 특성때문에 제너레이터는 지연 평가(Lazy Evaluation) 구현체 라고 부르기도 합니다.

직역하면 제너레이터는 게으르다는 뜻인데 이게 당최 뭔소리일까요?

쿨가이 제너레이터?

일반적인, 그러니까 lazy 하지 않은 객체를 먼저 생각해봅시다.

가장 대표적으로 list를 한번 볼까요?

_list = [1,2,3,4,5]

모두 아시다시피 list객체의 각 요소는 이미 값이 정해져 있습니다.

0번 인덱스 : 1

1번 인덱스 : 2

2번 인덱스 : 3 ... 이렇게요.

이런 객체는 몇 번째 어떤 요소가 있는지 손쉽게 확인할 수 있다는 장점이 있지만 그 크기가 커질 수록 메모리의 낭비가 심해진다는 단점이 있습니다.

만약 list의 길이가 100만이 넘는다면? 100만개가 넘는 값을 모두 기억해야 하니 메모리가 살려달라고 몸부림 칠 지도 모릅니다.

죽..여줘..

하지만 lazy한 객체는 모든 요소를 저장하고 있지 않고

처음에 줘야 할 값, 그 다음부터 줘야 할 값, 값을 언제까지 줘야 하는지 에 대한 정보만을 가지고 있습니다.

이 정보들을 바탕으로 각 호출마다 값을 반환하게 되는 것입니다.

정리하자면 모든 값을 메모리에 올려두지 않고 필요할 때(호출할 때) 값을 평가해서 반환하는 방식입니다.

이 특성을 이용한 재미있는 예시를 하나 봅시다.

def endless_numbers():
    num = 2
    while True:
        yield num
        num = num + 2


foo = endless_numbers()

처음 반환할 값 : 2 (num의 초기값)

그 다음부터 줄 값 : num에 2를 더한 값

언제까지? : 명시되지 않았으므로 호출하는 만큼 계속

이렇게 생성된 foo 객체는 호출하는 만큼 짝수를 계속해서 반환합니다. 그리고 이 횟수는 이론상으로 100만, 1000만, 혹은 그 이상도 가능하죠.

같은 로직을 list로 구현하는것은 당연히 불가능하겠죠? 이것이 lazy한 객체의 특징이자 장점입니다.

따라서 불러올 다음 값에 대한 패턴이 존재한다면 lazy한 객체를 사용하는 것이 메모리 측면에서는 이득이라고 할 수 있습니다.

반응형