파이썬에서 보통의 함수는 값을 반환하고 종료하지만, 제너레이터 함수는 값을 반환하기는 하지만 산출(yield)한다는 차이점이 있다. 쉽게 말하면 제너레이터는 iterator
를 생성해주는 함수라고 볼 수 있다. 다음 코드를 확인해보자.
def generator_squares():
for i in range(3):
yield i ** 2
print("gen object=", end=""), print(generator_squares())
gen object=<generator object generator_squares at 0x10f3b0150>
yield
는 제너레이터 함수에서 값을 반환할 때 사용되며 yield
호출 후에 다시 next
가 호출될 때 까지 현재의 상태에서 머물고 있다가 next
함수가 호출되면 이전의 상태에 이어서 다음 연산을 수행한다.
dir
로 함수 종류를 확인해보면 iter
와 next
함수 둘 다 존재하는 것을 확인할 수 있다.
print("dir gen =", end=""), print(dir(generator_squares()))
dir gen =['__class__', '__del__', '__delattr__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__',
'__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
그래서 이터레이터에서 처럼 __iter__
를 호출한 후에 __next__
함수를 호출하지 않아도 __next__
를 바로 호출할 수 도 있다. 다음은 iter
와 next
함수를 사용한 제너레이터 함수의 코드다.
gen = generator_squares()
print(gen.__next__())
print(gen.__next__())
print(gen.__next__())
print(gen.__next__())
출력결과
0
1
4
Traceback (most recent call last):
File "generator.py", line 14, in <module>
print(gen.__next__())
StopIteration
제너레이터 함수는 실행중에 send
함수를 통해서 값을 전달할 수도 있다.
def generator_send():
received_value = 0
while True:
received_value = yield
print("received_value = ",end=""), print(received_value)
yield received_value * 2
gen = generator_send()
next(gen)
print(gen.send(2))
next(gen)
print(gen.send(3))
출력결과
received_value = 2
4
received_value = 3
6
generator_send
함수는 yield
로 send
를 통해서 받은 값을 received_value
에 할당하고 그 값의 2배 수를 리턴받고 있다.
제너레이터에서는 이처럼 yield
를 이용해서 제너레이터 함수 실행 중에 값을 전달 할 수 있고, 응용하면 제너레이터 함수를 사용해서 main 실행 루프에서 연산결과에 따라 호출도 제어할 수 있다.
이외에도 제너레이터 표현식(generator experession)이 있는데, 제너레이터 표현식은 Lazy evaluation
을 위해서 사용될 수 있다.
제너레이터 표현식은 리스트 컴프리헨션과 표현식이 비슷하지만 중괄호가 아닌 소괄호()를 사용한다는 차이점이 있다.
Lazy evaluation
을 알아보기 위해 다음 코드를 살펴보자.
import time
L = [ 1,2,3]
def print_iter(iter):
for element in iter:
print(element)
def lazy_return(num):
print("sleep 1s")
time.sleep(1)
return num
print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)
print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)
출력결과
sleep 1s
sleep 1s
sleep 1s
1
2
3
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3
python 내장함수 time을 import한 뒤 lazy_return
함수에 1초 지연을 적용시켰다. comprehension_list
는 대괄호로 리스트 컴프레헨션을 적용해서 1초 간격으로 sleep 1s 문장이 출력된 후 1,2,3의 결과가 한꺼번에 출력이 되었다.
generator_exp
는 소괄호로 제너레이터 표현식을 적용해서 sleep 1s와 숫자가 같이 출력된 뒤에, 1초 간격으로 후속 문장이 출력되었다.
Lazy evaluation
은 직역하면 '느긋한 계산'인데, 달리 말하면 '필요한 것'만 계산한다는 의미다. 컴파일러가 무한하게 반복하는 객체나 크기가 큰 객체에서 필요없는 계산을 하지 않음으로써 메모리 값을 아낄 수 있어서 성능의 이득을 취할 수 있다.
Lazy evaluation
을 사용하면 무한 자료 구조의 일부
를 사용할 수 있는데 다음 예시를 살펴보자.
def addOne(n):
[n] + addOne(n + 1)
list = addOne(1) // [1, 2, 3, 4, 5, 6, …]
oneToThree = list.takeFirst (3)
print (oneToThree)
print (range (5, 10))
출력결과
[1, 2, 3]
[5, 6, 7, 8, 9]
무한하게 반복하는 자료구조인 addOne
함수에서 print (range (5, 10))
와 같이 범위를 지정하여 [5, 6, 7, 8, 9]의 결과값을 산출하였다. 위처럼 무한한 데이터 중에서 특정 데이터를 선택하거나 혹은 데이터의 범위를 정하여 그 일부를 사용할 수 있다.
이제 다시 generator_exp
으로 돌아가보자.
리스트 컴프리헨션을 사용한 comprehension_list
는 sleep
을 통해 지연이 적용된 첫번째부터 3번째 문자열을 제외하고 1,2,3은 한꺼번에 출력이 되었다. 이는 컴파일러가 sleep
이전에 해당 print 명령을 수행해 놓았고, sleep
이 끝나자 한번에 출력한 것이다.
그러나 generator_exp
는 '필요한 계산'만 수행해서, print_iter
함수를 미리 수행하지 않고, lazy_return
함수가 수행될 때마다 하나씩 호출하여 수행한 것이다. 따라서 lazy_return
함수의 print
문인 sleep 1s와 숫자가 같이 출력된 뒤에, 1초 간격으로 print_iter
의 print
문이 출력되는 결과가 나온 것이다.