이터레이터를 생성해주는 함수
이터레이터는 클래스에 __iter__
, __next__
또는 __getitem__
메소드를 구현해야하지만 제너레이터는 함수 안에서 yield
라는 키워드만 사용하면 된다. 따라서 제너레이터를 사용하면 이터레이터보다 훨씬 간단하게 작성할 수 있다.
함수 안에서 yield
를 사용하면 함수는 제너레이터가 되며 yield
에는 값(변수)을 지정한다.
제너레이터는 제너레이터 객체에서 __next__
메소드를 호출할 때마다 함수 안의 yield
까지 코드를 실행하며 yield
에서 값을 발생시킨다. 그래서 제너레이터라고 부른다.
(※ 제너레이터 함수 호출 > 제너레이터 객체 > __iter__
는 self 반환 > 제너레이터 객체)
yield
: 생산하다, 양보하다
즉, yield
를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보한다. 따라서, yield
는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만든다. (return은 반환 즉시 함수가 종료됨)
yield를 사용하여 바깥으로 전달한 값은 next 함수(__next__
)의 반환값으로 나온다.
제너레이터 함수는 실행중에 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 실행 루프에서 연산결과에 따라 호출도 제어할 수 있다.
제너레이터는 함수 끝까지 도달하면 StopIteration
예외가 발생한다. 마찬가지로 return
도 함수를 끝내므로 return
을 사용해서 함수 중간에 빠져나오면 StopIteration
예외가 발생한다. 특히, 제너레이터 안에서 return
에 반환값을 지정하면 StopIteration
예외의 에러 메시지로 들어간다.
제너레이터 함수외에도 제너레이터 표현식(Generator expression)이 있는데, 제너레이터 표현식은 지연 실행(Lazy evaluation)을 위해서 사용될 수 있다.
제너레이터 표현식의 문법은 list comprehension문법과 비슷하지만 []
가 아닌 ()
를 사용하여 만든다. 다음 코드는 generator expression을 사용해서 제곱연산을 하는 예제다.
L = [ 1,2,3]
def generate_square_from_list():
result = ( x*x for x in L )
print(result)
return result
def print_iter(iter):
for element in iter:
print(element)
print_iter( generate_square_from_list() )
<generator object generate_square_from_list.<locals>.<genexpr> at 0x7ff6af079ac0>
1
4
9
다음 코드를 실행해보고 분석한 결과를 적어보자. (lazy evaluation이란 무엇인지, 장점 및 list comprehension과의 차이점에 대해서도 서술)
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)
어떤 값이 실제로 쓰일 때 까지 그 값의 계산을 뒤로 미루는 동작 방식
실제로 값을 사용될 때만 사용하기 때문에 시간과 메모리 절약할 수 있다(미리 연산해놓지 않고 미리 저장해놓지 않기 때문에)는 장점이 있다.
위의 코드를 토대로 generator expression과 list comprehension을 비교해보자.
list comprehension
[]
로 묶는다.
집에 열명이 올지 말지 정확히 모르는 상태에서 일단 라면을 열봉지를 끓여놓고 기다린다. 어쩌면 라면을 다 버리게 될 수 도 있다. 이게 list comprehension이다. 뒷일을 생각하지 않고 우선 연산한다.
실제 print
를 하기 전부터 실제 사용 될지에 대한 판단도 없이 우선 lazy_return()
의 기능을 수행한다. time.sleep(1)
가 먼저 세번 수행이 되고 나서print(element)
가 출력된 것을 보아 알 수 있다.
generator expression
()
로 묶는다.
집에 열명이 올지 안올지 모르는 상태이니 먼저 라면을 다 끓여놓지 말고 몇명이 올지 확정이 되면 바로 라면을 끓일 수 있도록 끓일 준비만 다 해놓는다. 이게 generator expression이다.
실제로 lazy_return()
함수가 우선 값을 미리 만들어놓는 것이 아니라 값을 사용하는 순간에만 함수를 수행하고 있음을 확인할 수 있다. 그렇기 때문에 출력 결과에서 time.sleep(1)
과 print(element)
번갈아가면서 출력되고 있다.
값이 실제로 사용되지 않으면 연산 또한 하지 않으므로 시간과 메모리를 절약할 수 있다. 하지만 그렇다고 무조건적으로 generator expression이 옳다는것은 아니다. 모든 요소나 대부분의 요소가 사용될 것이 확실한 상황이라면 list를 통해 미리 연산을 해두는 것이 더 효율적이니 상황에 맞게 사용할 수 있도록 하자.
📕 참고한 블로그를 읽고 이해가 많이 됐다.😂