그냥 간단하게 생각하면 된다. 우리가 잘하는 나중에 하기 이다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 🤣🤣. 좀 더 컴퓨터적으로 생각해보자. 나중에 필요할 값을 한 번에 만들어두면 메모리를 많이 사용하게 되어 성능에도 불리하다. 그래서 파이썬에서는 이터레이터만 생성하여 값이 필요한 시점에 산출(yield) 하여 사용한다. 즉, 데이터 생성을 뒤로 미루는 것인데 이런 방식을 Lazy Evaluation 라고 부른다.
그리고 이런 이터레이터를 생성하는 함수가 제너레이터이다.
일단 장점은 위에서 언급했듯이 메모리를 효율적으로 사용한다는 점이다. 그럼 어떻게 효율적인지 비교해 보고싶다 (그냥 그렇다고 하자 👻). 그 비교 대상으로 리스트 컴프리헨션이 적당하다. 크기를 비교해보자!
>>> import sys
>>> sys.getsizeof([i for i in range(10000)]) # 리스트 컴프리헨션
87632
>>> sys.getsizeof((i for i in range(10000))) # 제너레이터 컴프리헨션
128
와우 😱😱😱, 차이가 눈에 보이지 않는가!! 리스트는 사이즈가 커지면 메모리 사용량이 늘어나지만 제너레이터는 메모리 사이즈가 항상 동일하다. 제너레이터는 next()
메서드를 통해 값에 접근할 때마다 메모리에 적재하는 방식이기 때문이다. 즉, 리스트의 규모가 클 수록 제너레이터의 효율성이 더욱 높아진다.
이번 과제는 아래의 코드를 실행해보고 분석한 것을 포스팅하는 것이다. 그럼 코드를 보자!
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)
일단 여기서는 lazy_return()
함수에서 sleep()
을 사용하여 1초지연 시킨다는 점을 기억한다.
여기서 일단 기억을 되살려야하는 부분이 있다.
실제 for 루프에 Iterable Object를 사용하면, 해당 Iterable의 __iter__() 메서드를 호출하여 iterator를 가져온 후 그 iterator의 __next__() 메서드를 호출하여 루프를 돌게 된다.
위의 사실을 다시 한 번 인지하고 아래를 읽기바란다.
print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)
lazy_return()
함수가 리스트의 길이만큼 실행된다.print_iter()
함수의 인자로 comprehension_list를 전달하여 반복문을 돌아 아이템들을 출력한다.결과를 보면 다음과 같이 출력된다.
comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3
print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)
제너레이터가 두 번째 줄에서 생성되었다. 해당 컴프리헨션 코드는 다음과 똑같다.
def generator_exp_func_ver(L):
for i in L:
yield lazy_return(i)
그래서 세 번째 줄은 다음과 같은 코드로 대체될 수 있다.
print_iter(generator_exp_func_ver(L))
아무튼 Lazy Evaluation 의 영향으로 for문에서 __next__()
를 호출 할 때 마다 lazy_return()
함수가 실행되게 된다.
아래의 출력값은 다음과 같다.
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3