Yield란 일종의 생성자(Generator)와 같습니다.

이더레이터(iterator)와는 비슷한 결과값을 가져오지만, 다른 것입니다.

이더레이터는 공부하다보니 알게 되었습니다. 일단 먼저 yield를 살펴보도록 하겠습니다.


먼저 Python docs에서 Generator의 정의를 보도록 합시다.



generator


  A function which returns an iterator. It looks like a normal function except that it contains yield statements for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function. Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator resumes, it picks-up where it left-off (in contrast to functions which start fresh on every invocation).



Generator는 Iterator를 생성해주는 함수라고 간단하게 설명할 수 있겠습니다. iterator는 next()함수를 통해 순차적으로 값을 가져오는 object를 말합니다.

(LINK : Iterator & Iterable에 대한 설명)


Generator는 일반함수릉 크게 다를 것은 없지만 yield라는 가장 큰 차이점을 가지고 있습니다. 

아래의 소스는 yield의 기본 예제입니다.

#python 3 version source
#yield generator test source
#yield_Basic_Test.py

def number_generator(n):
	print("Function Start")
	while n < 6:
		yield n
		n += 1
	print("Function End")
	
if __name__ == "__main__":
	for i in number_generator(0):
		print(i)
		


결과 값은 다음과 같습니다.

[그림 01] yield_Basic_Test.py 결과값


for문과 number_generator(n)함수의 합이 만나서 [그림 01]과 같은 결과값이 나옵니다.

number_generator()함수에는 return이 없는데 어떻게 저렇게 while문 내에서 돌아가는 n의 값이 출력이 되는 것일까요?

기존의 함수의 Call 모습과 yield가 포함된 함수의 Call 모습을 비교해보도록 하겠습니다.


[그림 02] Main에서 Normal_function으로의 함수 흐름


Main함수에서 Normal_function으로의 함수 흐름은 기존의 여타 함수의 흐름과 다를 게 없습니다.

Main을 실행하다가 Normal_function으로 가서 함수 내의 루틴을 실행시킨 후 Main으로 돌아가 남은 Main의 루틴이 실행됩니다.

그렇다면 이제 yield가 포함된 함수를 보도록 합시다.


[그림 03] Main에서 number_generator()으로의 함수 흐름


number_generator()으로의 함수 흐름이 일반 함수의 흐름과는 많이 다릅니다.

이 흐름은 yield라는 것 때문입니다. yield는 두 번째 흐름처럼 n의 값을 return 하고 다시 함수로 돌아가게끔해주는 것입니다.

그렇다면 다음 소스를 보겠습니다.


Python docs에서 정의된 yield의 정의를 보도록 합시다.



Yield


The yield statement is only used when defining a generator function, and is only used in the body of the generator function. Using a yield statement in a function definition is sufficient to cause that definition to create a generator function instead of a normal function.


When a generator function is called, it returns an iterator known as a generator iterator, or more commonly, a generator. The body of the generator function is executed by calling the generator’s next() method repeatedly until it raises an exception.


When a yield statement is executed, the state of the generator is frozen and the value of expression_list is returned to next()‘s caller. By “frozen” we mean that all local state is retained, including the current bindings of local variables, the instruction pointer, and the internal evaluation stack: enough information is saved so that the next time next() is invoked, the function can proceed exactly as if the yield statement were just another external call.


As of Python version 2.5, the yield statement is now allowed in the try clause of a try ... finally construct. If the generator is not resumed before it is finalized (by reaching a zero reference count or by being garbage collected), the generator-iterator’s close() method will be called, allowing any pending finally clauses to execute.



yield는 generator가 일반 함수와 구분되는 가장 핵심적인 부분입니다. yield를 사용함으로써 어떤 차이가 있는지는 위의 일반 함수와 yield가 포함된 함수의 흐름으로 설명 드렸습니다.

더 자세하게 설명드리자면, generator 함수가 실행 중 yield를 만날 경우, 해당 함수는 그 상태로 정지되며, 반환 값을 next()를 호출한 쪽으로 전달하게 됩니다. 이후 해당 함수는 일반적인 경우처럼 종료되는 것이 아니라 그 상태로 유지되게 됩니다. 즉, 함수에서 사용된 local 변수나 instruction pointer 등과 같은 함수 내부에서 사용된 데이터들이 메모리에 그대로 유지되는 것입니다.   [bluese05님 글 출처]


#python 3 version source
#yield test source
#yield_Routine_Test.py

def generator_test(n):
	print("-=-=-=-=-=-= Generator Start =-=-=-=-=-=-")
	
	while(n < 3):
		print("<< Before Yield >>")
		yield n
		n += 1
		print("<< After Yield >>")
		
	print("-=-=-=-=-=-= Generator End =-=-=-=-=-=-")
	
if __name__ == "__main__":
	print("---------- Main Function Start ----------")
	
	for i in generator_test(0):
		print("Start For))))))))))))")
		print("Yield i is : ", i)
		print("End For))))))))))))))")
		
	print("---------- Main Function End ----------")
		

이 소스는 Main 함수와 generator_test()함수의 흐름을 보기 위한 함수입니다.

결과 값을 보도록 합시다.

[그림 04] yield_Routine_Test.py


결과값을 보면 먼저 Main이 시작하고 끝은 Main으로 끝납니다.

그리고 Generator 함수의 시작과 끝도 양 끝에 있습니다.

그렇다면 루틴은 <Before yield> -> for문 시작 -> for문 내의 print 실행 -> for문 끝 -> <After yield>로 되어 있고 이렇게 3회 반복됩니다.

이 루틴을 그림으로 그려보면 [그림 05]와 같습니다.


[그림 05] Main에서 for문으로 된 generator 함수의 흐름


먼저 Main이 실행되면서 for문 내의 generator_test함수로 들어갑니다. 그리고 함수가 시작되면서 while문으로 들어가 쭈욱 실행하면서 yield에서 n이라는 변수에 저장된 값을 반환합니다. 그리고, for문 내의 루틴이 실행된 후(print함수를 실행), 다시 yield 함수의 아래의 루틴을 실행 후 다시 while문이 실행되면서 루틴이 반복됩니다.

설명을 겁나 어렵게 했네요. 가볍게 <Before yield> -> for문 시작 -> for문 내의 print 실행 -> for문 끝 -> <After yield>로 루틴이 이루어져 있고, 그 겉에 Main과 generator함수가 있다고 보시면 됩니다.


자 이제, Python docs에서 Generator expression을 살펴보도록 하겠습니다.



generator expression


  An expression that returns an iterator. It looks like a normal expression followed by a for expression defining a loop variable, range, and an optional if expression. The combined expression generates values for an enclosing function:



generator expression이란 generator 함수를 좀 더 쉽게 사용할 수 있도록 제공되는 표현형식입니다. list comprehension과 비슷하지만, [] 대신 ()를 사용하면 됩니다. 아래의 소스는 0 ~ 9 사이 정수 중 홀수를 list와 generator object로 생성한 예제입니다.

[i for i in range(10) if i % 2]
#[1,3,5,7,9]
(i for i in range(10) if i % 2)
#<generator object at 0x......>


활용해보면 다음과 같은 소스와 결과를 볼 수 있습니다.

#python 3 version source
#Yield_Test.py

def generator(wordList):
	wordListLength = 0
	print("Generator Start!!")
	while(wordListLength < len(wordList)):
		yield wordList[wordListLength]
		wordListLength += 1
		
	
if __name__ == "__main__":
	Main_wordList = ["TEST", "IS", "WORK", "GOOD"]
	for i in generator(Main_wordList):
		print(i)
		
	print("Done!!!")


[그림 06] yield_Test.py 결과값





generator를 사용하는 이유는 크게 두 가지로 구분할 수 있습니다.


1. Memory를 효율적으로 사용할 수 있음

list comprehension과 generator expression으로 각각 생성했을 때 메모리 사용 상태를 봅시다.

#python 3 version 기준
import sys

a = [i for i in range(100) if i % 2] # list 
sys.getsizeof(a)
#268

b = [i for i in range(1000) if i % 2]
sys.getsizeof(b)
#2140

c = (i for i in range(100) if i % 2) # generator
sys.getsizeof(c)
#48

d = (i for i in range(1000) if i % 2)
sys.getsizeof(d)
#48


list의 경우 사이즈가 커질 수록 그만큼 메모리 사용량이 늘어납니다. 하지만, generator의 경우는 사이즈가 커진다해도 차지하는 메모리 사이즈는 동일한 것을 볼 수 있습니다. list와 generator의 동작 방식의 차이가 있을 뿐입니다.


list는 list 안에 속한 모든 데이터를 메모리에 적재하기 때문에 list의 크기 만큼 차지하는 메모리 사이즈가 늘어나게 됩니다. 하지만 generator의 경우 데이터 값을 한 번에 메모리에 적재하는 것이 아니라 next() 함수를 통해 차례로 값에 접근할 때마다 메모리에 적재하는 방식입니다.  [bluese05님 글 출처]


2. Lazy evaluation 즉 계산 결과 값이 필요할 때까지 계산을 늦추는 효과가 있음

예제로써 설명하도록 하겠습니다.

먼저 함수는 sleep_function(x)라고 선언하도록 합시다.

def sleep_function(x):
	print("Sleep..")
	time.sleep(1)
	return x


sleep_function()함수는 1초간 sleep을 수행한 후 x값을 return하는 함수입니다. 만약 위 sleep_function()함수를 이용해 list와 generator를 생성하면 어떻게 동작될지를 다음의 예제로 설명하도록 하겠습니다.

import time

def sleep_function(x):
	print("sleep...")
	time.sleep(1)
	return x

# list 생성
list = [sleep_function(x) for x in range(5)]

# generator 생성
generator = (sleep_function(y) for y in range(5))

for i in list:
	print(i)
	
print("=-=-=-=-=-=-=-=-=-=-=-=-=-=")

for j in generator:
	print(j)


위의 소스를 실행시키면 list생성과 generator 생성을 통해 각각 다른 방법으로 결과값을 받아오는 것을 알 수 있습니다.

결과 값은 [그림 07]과 같습니다.


[그림 07] list와 generator의 실행 차이


list의 경우는 list comprehension을 수행할 때, list의 모든 값을 먼저 수행하기 때문에 sleep_function() 함수를 range()만큼 한 번에 수행하게 됩니다.

generator의 경우 생성 시 실제 값을 로딩하지 않고, for문이 수행될 때 하나씩 sleep_function()을 수행하며 값을 불러오게 됩니다. generator의 특징 중 하나는 "수행 시간이 긴 연산을 필요한 순간까지 늦출 수 있다는 점이 특징"이라고 할 수 있습니다.


간단하게 말하면, list는 실행되고 결과 값을 list에 반환되어 [0,1,2,3,4] 값을 가지고 있게 됩니다.

그러나 generator는 실행되지 않고 object로 선언이 되기 때문에 필요한 때마다 실행시킬 수 있게 된 것입니다.


[그림 08] list comprehension과 generator expression 차이



참고 URL : haerakai님의 글

참고 URL : bluese05님의 글



+ Recent posts