이 글의 작성자는 C/C++ 프로그래밍을 하던 사람이다. 이 글은 Python을 복습하며 작성하는 글이니, 부족한 부분이 있으면 얼마든지 피드백을 주시기 바란다.

 

제너레이터(generator)

리스트 컴프리헨션은 다음과 같이 만들었었다.

[expression for item in iterable]

그러면 혹시 대괄호를 소괄호로 바꾸면 튜플 컴프리헨션이 되지 않을까? 라는 생각을 해 볼수 있다. 그렇다면 아마 이런 식으로 표현할 수 있을 것이다.

(expression for item in iterable)

그러면 한번 테스트해보자.

t = (v for v in range(5))
print(t)

이러면 아마 다음과 비슷한 문장이 출력될 것이다.

<generator object <genexpr> at 0x03C85E60>

뭔가 이상한게 튀어나왔다. 우리가 기대한건 (0,1,2,3,4)였는데, 무슨 generator라는 이상한 무언가가 튀어나왔다.

 

여러분은 방금 제너레이터(generator)라는 것을 만들었다. generator 단어는 발생시키다, 만들어내다 라는 단어인 generate에서 따온것으로 보인다. 그리고 그 행위를 하는 무언가를 나타내는 er(or)을 붙여서 generator가 된 것이다.

 

왜 이름이 제너레이터일까? 실제로 값을 만들어 주기 때문이다. 물론 정확히 말하면 이터레이터(iterator : 순회자)를 만들어 주는 것이긴 하다. 이터레이터에 관한 내용은 다음에 알아보기로 하고, 우선은 제너레이터가 어떻게 생긴 녀석인지 알아보도록 하자.

 

제너레이터 만들기

사실 위에서 생성한 방식은 좀 특이한 방식이었다. 마치 리스트 생성법을 가르쳐주는데, 처음부터 리스트 컴프리헨션을 소개하는 꼴이었다.

그럼 일반적인 제너레이터 생성 방법은 무엇일까? yield 키워드가 하나라도 들어있는 함수를 호출하면 제너레이터가 만들어진다. yield가 뭐길래 이게 들어있으면 제너레이터가 되는지는 잘 모르겠지만, 일단 대충 yield 키워드를 넣어서 제너레이터라는 것을 만들어보자.

yield는 return처럼 해당 키워드 뒤에 객체를 쓰면 된다.

def gen():
    yield 1
    yield 2
    yield 3

print(gen())

위의 코드를 실행하면 다음과 같은 결과가 나올 것이다.

<generator object gen at 0x01DE9D80>

제너레이터 객체가 생성된 것을 확인할 수 있다.

 

제너레이터 사용하기

그러면 이렇게 생성된 제너레이터는 어떻게 써야 할까?

  • 반복문에서 사용하기

제너레이터는 순회 가능한 객체(iterable)다. 그렇다는 것은? 반복문에 집어넣을 수 있다는 이야기이다. 그러면 위의 코드에서 print(gen())을 다음과 같이 바꿔보자.

for v in gen():
    print(v)

그러면 1,2,3값이 순서대로 나올 것이다. 이 값은 어디서 본 값인가? gen 함수에서 yield 시킨 값이다. 이리 바꿔보고 저리 바꿔보면 yield 된 순서대로 나오는 것을 확인할 수 있을 것이다.

  • next() 사용하기

반복문을 사용하면 순회가 끝날 때까지 계속해서 동작하게 될 것이다. 만약 값을 하나만 가져오고 싶다면, next()를 사용하면 된다. Python 내장 함수이므로 그냥 써도 된다.

그러면 제너레이터에서 딱 하나의 값만 가져와서 출력해보자.

g = gen()
print(next(g))

이러면 1만 출력되는 것을 확인할 수 있을 것이고, print(next(g))를 계속해서 붙여 넣다보면 2와 3까지 출력되는 것을 확인할 수 있을 것이다. 그리고 한계를 넘어서면 StopIteration 이라는 에러가 발생하는 것을 볼 수 있을 것이다. 대충 “더 이상은 순회할 수 없다”라는 뜻 같아보인다.

 

제너레이터 동작 분석해보기

그러면 차근차근 제너레이터의 동작을 분석해보자.

 

gen과 gen()의 차이

당연한 이야기일수도 있지만, yield 키워드가 들어있다고 해서 함수 자체가 제너레이터가 되는 것이 아니다. gen 함수는 그대로 함수 객체이다. 하지만 gen 함수를 실행한 결과인 gen()은 제너레이터 객체가 된다. 따라서, gen() 함수를 여러번 실행하면 저 마다의 다른 제너레이터 객체가 생성될 것이다.

a = gen()
b = gen()
print(id(a), id(b))

위의 두 코드에서 id 값은 다른 값이 나올 것이다.

 

yield

줄곧 거슬렸던 것이 있다. 그래서 yield가 뭐하는 키워드인가? 라는 질문이 남아있다.

대충 감이 온 사람들도 있을 수 있겠지만, yield는 yield한 값을 돌려주고, 잠깐 그 자리에서 쉬는 역할을 수행한다.

그러면 gen 함수에서 yield를 하기 전에 무엇을 yield할 것인지를 출력하도록 바꿔보자. 그래서 실제로 yield가 어떻게 돌아가는지 확인해보자.

def gen():
    print('1을 yield')
    yield 1
    print('2를 yield')
    yield 2
    print('3을 yield')
    yield 3
    print('끝!')


for v in gen():
    print(v)
print('끝났어요!')

출력 결과는 다음과 같다.

1을 yield
1
2를 yield
2
3을 yield
3
끝!
끝났어요!

출력된 순서를 보면 코드의 흐름은 다음과 같이 되었음을 추측할 수 있다.

  • gen()을 통해 gen 함수 기반 제너레이터가 생성되었다.
  • print(‘1을 yield’)를 실행하였다.
  • yield 1을 만나서 값 1을 반환하고, 여기서 잠시 멈춘다.
  • v가 1인 채로 print(v)가 실행되어, 1이 출력되었다.

  • 다시 순회로 돌아가서, 멈춘 지점인 yield 1 다음에 있는 문장, print(‘2를 yield’)를 실행하였다.
  • yield 2를 만나서 값 2를 반환하고, 여기서 잠시 멈춘다.
  • v가 2인 채로 print(v)가 실행되어, 2가 출력되었다.

  • 다시 순회로 돌아가서, 멈춘 지점인 yield 2 다음에 있는 문장, print(‘3을 yield’)를 실행하였다.
  • yield 3을 만나서 값 3을 반환하고, 여기서 잠시 멈춘다.
  • v가 3인 채로 print(v)가 실행되어, 3이 출력되었다.

  • 다시 순회로 돌아가서, 멈춘 지점인 yield 3 다음에 있는 문장, print(‘끝!’)를 실행하였다.
  • 제너레이터 코드가 끝나서 for 문은 끝나고, 마지막 문장인 print(‘끝났어요!’)가 출력되었다.

즉, 제너레이터는 자신의 생성한 함수의 코드를 그대로 따라가다가, yield를 만나면 그 값을 돌려주고, 그 자리에서 멈춘다. 그리고 다시 값을 전달 받기를 요청받으면, 멈췄던 자리에서 다시 코드를 실행하게 된다. 그리고 이 동작은 return 문을 만나거나, 함수의 코드가 끝날 때까지 계속된다. 따라서 제너레이터 생성 함수가 끝나지 않는 무한 루프라면, 해당 제너레이터는 영원히 값을 생성하게 되는 것이다.

def infinite():
    v = 0
    while True:
        yield v
        v += 1

이 함수로 생성된 제너레이터는 0,1,2,3,4… 순으로 계속해서 값을 생성하여 돌려줄 것이다.

 

Lazy Evaluation

lazy가 무슨 뜻인가? 게으르다 라는 뜻이다. 왜 제너레이터보고 게으르다고 이야기하는 것일까?

제너레이터는 생성이 된다고 해서 값을 무작정 생성하지 않는다. 시험 공부는 시험 기간 직전에 하는 우리들의 모습을 보듯이, 제너레이터는 평소에는 느긋하고 놀고 있다가, 값이 필요하다는 요청을 받았을 때에만 함수를 통해 yield할 값을 생성한다.

‘게으른게 뭐가 좋냐?’ 라는 말을 할 수도 있지만, 그렇다고 쓸지 안쓸지 모르는 값들을 미리 만들어 두는 것도 딱히 좋지는 않다. 시험 범위는 1장에서 3장까지인데, 미리미리 10장까지 공부할 필요가 있는가? 특히나 위의 예시와 같이 생성할 수 있는 값의 범위가 무한하면 미리 생성하지도 못한다. 따라서 필요할 때만 값을 생성하는 방법은 꽤나 효율적인 방법이다.

이러한 점은 효율적인 메모리 사용측면에서도 좋다고 볼 수 있겠다. 만약 range()같이 특정 수들에 대해 순회를 하려고 한다고 해 보자. 리스트로 할 때와 제너레이터로 할 때의 차이는 어떻겠는가?

1부터 10000까지 순회를 한다고 생각해보자. 리스트는 [1,2,3,4…,10000] 까지의 값을 모두 집어넣고 있어야 하는 반면, 제너레이터는 1,2,3,4…,10000의 값을 반복문을 이용하여 순서대로 yield하는 함수를 작성하면 그만이다. 1만개의 정수 객체 vs 하나의 제너레이터 객체. 무엇이 더 효율적이겠는가?

 

제너레이터는 효율적으로 순회할 수 있는 객체를 만들 수 있는 수단이다. 비록 리스트나 튜플처럼 임의 접근(random accesss)는 할 수 없지만, 임의 접근이 필요 없을 때에는 제너레이터를 사용하는 것도 고려해봄직 하다.

 

Categories:

Updated:

Comments