반응형

이번 글에서는 파이썬에서 제너레이터(generator)를 만드는데 사용되는 yield 키워드에 대해서 알아보겠다.

 

yield 키워드

대부분의 프로그래밍 언어에서 일반적으로 함수는 어떤 결과 값을 return 키워드를 이용해서 반환을 한다.
하지만, 파이썬에서는 함수가 yield 키워드를 이용해서 다소 다른 방식으로 결과 값을 제공할 수 있다.

백문이 불여일타다. 간단한 예제 코드부터 살펴보겠다.

알파벳 A, B, C 를 결과 값으로 반환하는 함수를 작성해보겠다.

def return_abc():
	return list("ABC")

위 함수를 yield 키워드를 이용해서 작성해보자.

def yield_abc():
    yield "A"
    yield "B"
    yield "C"

가장 먼저 눈에 두드러지는 차이는 return 키워드를 사용할 때는 결과값을 딱 한 번만 제공하는데, yield 키워드는 결과값을 여러 번 나누어서 제공한다는 것이다.

for 루프를 사용해서 위 함수를 호출하여 얻은 결과를 화면에 출력해 보자.

for ch in return_abc():
	print(ch)
A
B
C
for ch in yield_abc():
	print(ch)
A
B
C

함수를 사용하는 측면에서 보면 두 함수는 큰 차이가 없어보인다.

함수를 호출한 결과 값을 바로 출력하여 도대체 각 함수가 정확히 무엇을 반환하는지 알아보자.

return_abc() 함수 리스트(list)를 반환하고, yield_abc() 함수는 제너레이터(generator)를 반환한다는 것을 알 수 있다.

여기서 우리는 yield 키워드를 사용하면 제너레이터를 반환한다는 것을 알 수 있다. 과연 generator는 어떤 개념일까?

 

 

제너레이터(generator)

파이썬에서 제너레이터는 여러 개의 데이터를 미리 만들어 놓지 않고 필요할 때마다 즉석에서 하나씩 만들어낼 수 있는 객체를 의미한다.

예를 들어, 위에서 작성한 예제 코드를 알파벳 하나를 만드는데 1초가 걸리도록 수정 해보자.

import time

def return_abc():
    alphabets = []
    for ch in "ABC":
        time.sleep(1)
        alphabets.append(ch)
    return alphabets

위 함수를 호출한 결과를 for 루프로 돌려보면 3초가 흐른 후에 A, B, C 가 한 번에 출력이 되는 것을 볼 수 있다.

for ch in return_abc():
    print(ch)
# 3초 경과
A
B
C

 

이번에는 yield 키워드를 이용해서 동일한 결과 값을 제공하는 함수를 작성해보자.

import time

def yield_abc():
    for ch in "ABC":
        time.sleep(1)
        yield ch

위 함수를 호출한 결과를 for 루프로 돌로보면 1초 후에 A가 출력되고, 또 1초 후에 B가 출력되고, 또 1초 후에 C가 출력될 것이다.

for ch in yield_abc():
	print(ch)
# 1초 경과
A
# 2초 경과
B
# 3초 경과
C

만약, 세개의 알파벳이 아닌 백개, 천개, 만개의 알파벳을 제공해야 하는 경우에는 어떨까? 
첫 번째 방식에서는 첫 번째 방식을 얻는데 백초, 천초, 만초가 걸리는 반면에, 두 번째 방식에서는 항상 1초가 걸릴 것 이다. 
즉, 제너레이터를 통해서는 결과값을 나누어서 얻을 수 있기 때문에 성능 측면에서 큰 이점이 있다.

메모리 효율 측면에서도 이 두가지 방식은 큰차이를 보인다. 
return 키워드를 사용할 떄는 모든 결과 값을 메모리에 올려놓아야 하는 반면,
yield 키워드를 사용할 때는 결과 값을 하나씩 메모리에 올려놓는다.

제너레이터(generator)는 이러한 특성 때문에 흔히 게으른 반복자(lazy iterator)라고도 불린다.
이 제너레이터의 게으른 특성을 잘 활용하면 좀 더 효율적인 프로그램을 작성할 수 있는 경우가 많다.

특히 메모리에 한 번에 올리기에는 부담스럽게 대용량의 파일을 읽거나, 스트림 데이터를 처리할 때 상당히 유용하게 사용될 수 있다.

 

 

무한 데이터 생산

제너레이터를 사용하면 이론적으로 무한한 데이터를 계속해서 만들어낼 수도 있다.

예를 들어, 알파벳 A, B, C를 계속해서 무한하게 제공하는 함수를 작성해보겠다.

def yield_infinite_abc():
    while True:
        yield "A"
        yield "B"
        yield "C"

이 함수를 호출한 결과를 for 루프로 돌리면 A, B, C가 화면에 끊임없이 출력이 될 것이다. (ctrl+c 를 눌러서 빠져나올 수 있다.)

A
B
C
A
B
C
A
B
C
A
B
C
Traceback (most recent call last):"generator_yield.py", line 28, in <module>
    print(ch)
  File "/Users/kimmingyo/Library/Cloud
KeyboardInterrupt

이렇게 데이터를 무한하게 제공하는 함수는 사실 yield 키워드가 없이는 작성하는 것이 거의 불가능에 가깝다. 컴퓨터의 물리적인 메모리는 언제나 한계에 있으며 제 아무리 큰 리스트를 만들더라도 이 한계를 초과할 수는 없기 때문이다.

 

 

yield from

제너레이터를 반환하는 함수를 작성하다 보면 아래와 같이 리스트를 제너레이터로 변환해야 할 일이 자주 생긴다.

def yield_abc():
    for ch in ["A", "B", "C"]:
        yield ch

yield from를 사용하면 리스트를 바로 제너레이터로 변환할 수 있어서 매우 편리하니 한 번 꼭 사용해보길 바란다.

def yield_abc():
    yield from ["A", "B", "C"]

 

 

Generator Comprehension

지금까지는 함수 안에서는 yield 키워드를 사용해서 제너레이터를 만들어내는 방법에 대해서 살펴보았다.
제너레이터를 만드는 또 다른 방법으로 제너레이터 표현식(generator comprehension)이라는 것도 있다.

리스트 표현식(list comprehension)과 사용 방법이 매우 유사하다.
단지 차이점이라고 하면, 리스트 표현식은 대괄호를 사용하고 제너레이터 표현식은 소괄호를 사용한다는 것이다.

abc = (ch for ch in "ABC")

print(abc)

for ch in abc:
    print(ch)
<generator object <genexpr> at 0x7f891008f4a0>
A
B
C

 

python generator doc 자료

 

Generator Objects

Generator objects are what Python uses to implement generator iterators. They are normally created by iterating over a function that yields values, rather than explicitly calling PyGen_New() or PyG...

docs.python.org

+list comprehension 자료

 

반응형