All Articles

Python - generator

제너레이터(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 이후부터 이어서 실행한다.

즉 위 코드를 다시 뜯어보면,

  1. for문에서 numbers() 제너레이터 함수 호출
  2. numbers() 함수 안 첫 번째 반복문 제1턴 실행
  3. yield 키워드 만남
  4. 값 반환
  5. numbers() 함수 바깥으로 나가 외부 for문에 값 전달하여 print 실행
  6. 다시 numbers() 함수로 돌아와서 제2턴 실행
  7. 반복

의 과정을 거쳐 결과값을 반환한다.


제너레이터 표현식

제너레이터 표현식은 이터레이터를 반환한다. 제너레이터 함수를 좀 더 쉽게 사용할 수 있도록 해준다. 리스트 컴프리헨션과 구성이 비슷하나 리스트 컴프리헨션은 대괄호[]를 쓰고 제너레이터 표현식은 소괄호()를 쓴다.

(표현식 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>가 한 번 출력된 것을 확인할 수 있다.

printreturn을 실행한 뒤, 제너레이터가 반환한 값 1print_iter(iter)로 전달되어 1이 화면에 출력된다.

이후로는 제너레이터 표현식인 resultprint_iter(iter) 함수를 오가며 진행된다. 49는 이때 출력된다.

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
  1. print("comprehension_list=") 실행
  2. comprehension_list= 출력
  3. comprehension_list = [ lazy_return(i) for i in L ] 실행
  4. 첫 번째 값 1 가지고 comprehension_listlazy_return(num) 실행
  5. print("sleep 1s") 실행
  6. sleep 1s 출력
  7. time.sleep(1) 실행, 1초 기다림
  8. return num 실행해서 num1 반환
  9. 다음 값 2 가지고 comprehension_listlazy_return(i) 실행
  10. 5~8번 반복
  11. return num 실행해서 num2 반환
  12. 다음 값 3 가지고 comprehension_listlazy_return(i) 실행
  13. 5~8번 반복
  14. return num 실행해서 num3 반환
  15. comprehension_list = [1, 2, 3] 완성
  16. print_iter(comprehension_list) 실행
  17. comprehension_list 리스트 안에 있는 요소 1, 2, 3 출력

여기까지가

comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3

의 출력 과정이다. 이어서 계속 보자.

  1. print("generator_exp=") 실행
  2. generator_exp= 출력
  3. generator_exp = ( lazy_return(i) for i in L ) 실행
  4. 첫 번째 값 1 가지고 lazy_return(num) 실행
  5. print("sleep 1s") 실행
  6. sleep 1s 출력
  7. time.sleep(1) 실행, 1초 기다림
  8. return num 실행해서 num1 반환
  9. generator_exp 바깥으로 나감
  10. print_iter(generator_exp) 실행
  11. 요소 1 출력
  12. generator_exp = ( lazy_return(i) for i in L ) 실행
  13. 다음 값 2 가지고 lazy_return(num) 실행
  14. print("sleep 1s") 실행
  15. 22~24번 반복
  16. return num 실행해서 num2 반환
  17. 26~27번 반복
  18. 요소 2 출력
  19. generator_exp = ( lazy_return(i) for i in L ) 실행
  20. 다음 값 3 가지고 lazy_return(num) 실행
  21. 22~24번 반복
  22. return num 실행해서 num3 반환
  23. 26~27번 반복
  24. 요소 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) 함수가 훨씬 더 무거운 함수라고 가정해보자. 이때 리스트 컴프리헨션 연산 방식으로 반복문을 한 번에 처리하면 메모리가 그만큼 값을 저장하고 있으므로 서비스 성능을 저하시킬 수 있다. 제너레이터는 필요한 값을 그때그때 처리하기 때문에 메모리를 더 효율적으로 사용할 수 있다.