[Python] yield 문이란 무엇인가 ?!

파이썬 함수 안에서 yield 키워드를 사용하는 함수를 호출하면, 그 함수는 생성기(Generator) 객체를 반환합니다.

yield문을 이해하기 위해 먼저 생성기 객체에 대해서 알아보겠습니다.

생성기(Generator)는 무엇일까요?

for문과 같은 반복문에서 사용할 값들을 생성하는 객체입니다. 생성기 객체의 next()를 ( Python3 에서는 _next_() ) 호출하면 yield 문까지 함수가 실행되고 실행이 중단됩니다. 다음에 next()를 다시 호출하면 중단된 지점 다음부터 다시 함수가 실행됩니다.

예제를 통해서 생성기를 이해해 보도록 하겠습니다:

def yield_func():
    n = 0
    while n < 3:
        print('[yield_func] Start of loop. (n = {})'.format(n))
          print('[yield_func] Before yield. (n = {})'.format(n))
          yield n
          n += 1
          print('[yield_func] After yield. (n = {})'.format(n))
          print('[yield_func] End of loop. (n = {})'.format(n))

y = yield_func()
print('[main] yield가 반환한 객체 : {}'.format(y))
print('[main] 첫번째 next() 리턴값 : {}\n'.format(y.__next__()))
print('[main] 두번째 next() 리턴값 : {}\n'.format(y.__next__()))
print('[main] 세번째 next() 리턴값 : {}\n'.format(y.__next__()))
print('[main] 네번째 next() 리턴값 : {}'.format(y.__next__()))

실행 결과는 다음과 같습니다:

[main] yield가 반환한 객체 : <generator object yield_func at 0x7fcf500c1750>
[yield_func] Start of loop. (n = 0)
[yield_func] Before yield. (n = 0)
[main] 첫번째 next() 리턴값 : 0

[yield_func] After yield. (n = 1)
[yield_func] End of loop. (n = 1)
[yield_func] Start of loop. (n = 1)
[yield_func] Before yield. (n = 1)
[main] 두번째 next() 리턴값 : 1

[yield_func] After yield. (n = 2)
[yield_func] End of loop. (n = 2)
[yield_func] Start of loop. (n = 2)
[yield_func] Before yield. (n = 2)
[main] 세번째 next() 리턴값 : 2

[yield_func] After yield. (n = 3)
[yield_func] End of loop. (n = 3)
Traceback (most recent call last):
  File "ex1.py", line 16, in <module>
    print('[main] 네번째 next() 리턴값 : {}'.format(y.__next__()))
StopIteration

생성기 객체의 next() 함수를 처음 호출하면 처음부터 yield 문까지 함수가 실행되고 중단됩니다. 이후에 next() 문이 다시 호출되면 yield 문 이후부터 실행을 재개하여 다시 yield문을 만나면 실행이 중단됩니다. 반복이 종료되어 더 이상 yield문을 만날 기회가 없다면, StopIteration 예외를 발생시켜서 종료합니다.

그래서 yield문은 어디에 써먹는 걸까요?

아래 예제는 정수 배열을 만들어서 반환하는 함수입니다. 메인에서는 아주 큰 정수 값을 이 함수의 파라미터로 넣어서 반환된 배열의 각 요소의 합을 구합니다.

def custom_range(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums
### n 이 5이면 [0, 1, 2, 3, 4] 를 반환합니다.

sum_of_custom_range = sum(custom_range(1400000000))
### [0, 1, ..., 1399999998, 1399999999] 배열의 각 요소의 합을 구합니다.

이 경우 메모리에는 nums 라는 배열을 저장하기 위한 메모리가 항상 상주해있어야 합니다. 단순한 코드를 수행하기 위해 많은 메모리가 소비되어야 하는 구조입니다.

여기에서 yield문을 사용하여 생성기를 사용하는 코드를 적용하면 메모리의 사용을 대폭 줄일 수 있습니다. 다음과 같이 말입니다:

def custom_range_with_yield(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_custom_range_with_yield = sum(custom_range_with_yield(1400000000))

이렇게 yield문을 사용하면 지연된 생성 (lazy generation)을 수행함으로 모든 요소를 메모리에 올려둘 필요 없이 그때 그때 필요한 요소들을 생성하여 sum을 수행합니다. 따라서 메모리도 더 효율적으로 사용할 수 있습니다.

파이썬에선 위 함수와 같은 기능을 하는 range() (Python2 에서는 xrange()) 함수를 제공합니다. 항상 내장 함수의 성능이 더 좋다고 합니다.

하지만, 항상 list보다 생성기가 좋은 것은 아닙니다! 프로그램이 아래와 같이:

sum_of_custom_range_with_yield_1 = sum(custom_range_with_yield(1400000000))
sum_of_custom_range_with_yield_2 = sum(custom_range_with_yield(1400000000))

생성기를 통해서 똑같은 연산을 두 번 수행하는 경우에는 더 많은 연산이 필요하기 때문입니다. 이 경우 아래와 같이 리스트를 메모리에 상주해 두는 편이 더 나을 수도 있습니다.

nums = list(custom_range_with_yield(1400000000))
s = sum(nums)
p = product(nums)

하지만, 메모리의 중요성이 크지 않고, 같은 연산을 반복하는 것이 더 값 싼 경우라면 위와 같이 생성기를 사용하는 편이 더 나을 수도 있습니다.

지금까지는 yield문이 단독으로 나와서 값을 출력했습니다. 다음 포스팅에서는 대입 연산자( = )의 오른쪽에 yield가 나오는 경우 (코루틴과 yield 표현식) 에 대해서 포스팅해보겠습니다.

레퍼런스

kkamikoon 님의 글

파이썬 완벽 가이드

파이썬 위키

댓글

Designed by JB FACTORY