제너레이터(generator)
제너레이터는 이터레이터를 생성해주는 함수다. 함수 안에서 yield
라는 키워드를 사용하면 함수는 제너레이터가 되며, yield
에는 값(변수)을 지정한다.
yield
를 사용해서 제너레이터를 만들고, 숫자를 출력하는 for 반복문을 만들어보자.
def numbers():
for num in range(1, 11):
if num % 2 == 1:
yield num
for i in numbers():
print(i, end = " ")
# 출력값
1 3 5 7 9
제너레이터 함수의 작동 방식
일반적인 함수는 함수를 모두 실행한 뒤에 외부로 값을 반환하지만, 제너레이터 함수는 yield
가 나오는 순간 값을 반환한 뒤 잠시 연산을 멈추고 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한다. 그리고 다시 제너레이터 안의 코드 중 yield
이후부터 이어서 실행한다.
즉 위 코드를 다시 뜯어보면,
for
문에서numbers()
제너레이터 함수 호출numbers()
함수 안 첫 번째 반복문 제1턴 실행yield
키워드 만남- 값 반환
numbers()
함수 바깥으로 나가 외부for
문에 값 전달하여print
실행- 다시
numbers()
함수로 돌아와서 제2턴 실행 - 반복
의 과정을 거쳐 결과값을 반환한다.
제너레이터 표현식
제너레이터 표현식은 이터레이터를 반환한다. 제너레이터 함수를 좀 더 쉽게 사용할 수 있도록 해준다. 리스트 컴프리헨션과 구성이 비슷하나 리스트 컴프리헨션은 대괄호[]
를 쓰고 제너레이터 표현식은 소괄호()
를 쓴다.
(표현식 for 원소 in 반복 가능한 객체)
(표현식 for 원소 in 반복 가능한 객체 if 조건문)
generator_exp = (i for i in range(1,11))
print(generator_exp)
# 출력값
<generator object <genexpr> at 0x7fd638047c50>
generator_exp
가 제너레이터 객체임을 확인할 수 있다.
Q1. 다음코드는 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() )
A1.
# 출력값
<generator object generate_square_from_list.<locals>.<genexpr> at 0x7fd35820fbd0>
1
4
9
generate_square_from_list()
함수의 print(result)
코드와 return result
코드는 한 번 실행한 뒤 종료되었다. 따라서 print(result)
의 값인 <generator object generate_square_from_list.<locals>.<genexpr> at 0x7fd35820fbd0>
가 한 번 출력된 것을 확인할 수 있다.
print
와 return
을 실행한 뒤, 제너레이터가 반환한 값 1
이 print_iter(iter)
로 전달되어 1
이 화면에 출력된다.
이후로는 제너레이터 표현식인 result
와 print_iter(iter)
함수를 오가며 진행된다. 4
와 9
는 이때 출력된다.
Q2. 이번 과제는 다음코드를 실행해보고 분석한 결과를 블로깅하는 과제 입니다. lazy evaluation 이란 무엇인지와 장점 및 리스트 컴프리헨션과의 차이점에 대하여 블로깅 해주세요.
L = [1, 2, 3]
import time
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)
A2.
주어진 코드를 실행하면 아래와 같이 실행된다.
# 출력값
comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3
print("comprehension_list=")
실행comprehension_list=
출력comprehension_list = [ lazy_return(i) for i in L ]
실행- 첫 번째 값
1
가지고comprehension_list
안lazy_return(num)
실행 print("sleep 1s")
실행sleep 1s
출력time.sleep(1)
실행, 1초 기다림return num
실행해서num
값1
반환- 다음 값
2
가지고comprehension_list
안lazy_return(i)
실행 - 5~8번 반복
return num
실행해서num
값2
반환- 다음 값
3
가지고comprehension_list
안lazy_return(i)
실행 - 5~8번 반복
return num
실행해서num
값3
반환comprehension_list = [1, 2, 3]
완성print_iter(comprehension_list)
실행comprehension_list
리스트 안에 있는 요소1
,2
,3
출력
여기까지가
comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3
의 출력 과정이다. 이어서 계속 보자.
print("generator_exp=")
실행generator_exp=
출력generator_exp = ( lazy_return(i) for i in L )
실행- 첫 번째 값
1
가지고lazy_return(num)
실행 print("sleep 1s")
실행sleep 1s
출력time.sleep(1)
실행, 1초 기다림return num
실행해서num
값1
반환generator_exp
바깥으로 나감print_iter(generator_exp)
실행- 요소
1
출력 generator_exp = ( lazy_return(i) for i in L )
실행- 다음 값
2
가지고lazy_return(num)
실행 print("sleep 1s")
실행- 22~24번 반복
return num
실행해서num
값2
반환- 26~27번 반복
- 요소
2
출력 generator_exp = ( lazy_return(i) for i in L )
실행- 다음 값
3
가지고lazy_return(num)
실행 - 22~24번 반복
return num
실행해서num
값3
반환- 26~27번 반복
- 요소
3
출력
여기까지가
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3
의 출력 과정이다.
제너레이터는 일반적인 반복문과는 연산하는 절차가 다르다. 앞서 언급했듯 제너레이터는 값을 반환해야 할 때 제너레이터 안의 연산을 잠시 멈추고 외부로 값을 전달한다. 따라서 lazy_return(num)
함수에서 return
한 값을 가지고 있으면서 print_iter(iter)
함수를 처리해주는 과정을 반복한다.
반면 리스트 컴프리헨션은 lazy_return(num)
함수를 모두 처리한 후 print_iter(iter)
함수를 실행한다.
제너레이터는 lazy evaluation, 즉 지연 평가 방식으로 작동한다. lazy_return(num)
함수가 훨씬 더 무거운 함수라고 가정해보자. 이때 리스트 컴프리헨션 연산 방식으로 반복문을 한 번에 처리하면 메모리가 그만큼 값을 저장하고 있으므로 서비스 성능을 저하시킬 수 있다. 제너레이터는 필요한 값을 그때그때 처리하기 때문에 메모리를 더 효율적으로 사용할 수 있다.