본문 바로가기

Python

7-2. 제너레이터(Generator)와 yield 키워드

안녕하세요! 지난 시간에는 리스트와 딕셔너리 내포 표현식의 심화 활용법을 통해 코드를 더욱 간결하고 효율적으로 작성하는 방법을 알아보았습니다. 이제 여러분의 파이썬 코딩 생산성이 한 단계 더 높아졌을 거예요!

이번 시간에는 파이썬에서 메모리 효율적인 데이터 생성을 가능하게 하는 특별한 기능인 **제너레이터(Generator)**와 이를 만드는 핵심 키워드인 **yield**에 대해 알아보겠습니다.

제너레이터는 모든 데이터를 한꺼번에 메모리에 올려놓지 않고, 필요할 때마다 하나씩 값을 '생성'하여 반환하는 방식입니다. 마치 주문이 들어올 때마다 붕어빵을 하나씩 구워주는 붕어빵 장수와 같다고 생각하시면 됩니다. 이는 특히 대량의 데이터를 다룰 때 메모리 사용량을 획기적으로 줄여줄 수 있습니다.


Part 1: 제너레이터(Generator)란 무엇인가?

제너레이터는 **이터레이터(Iterator)**의 한 종류입니다. 이터레이터는 next() 함수를 호출할 때마다 다음 값을 순차적으로 반환하는 객체입니다. 제너레이터는 이러한 이터레이터를 간단하게 생성할 수 있는 특별한 함수입니다.

1. 제너레이터의 특징 및 필요성

  • 지연 평가 (Lazy Evaluation): 모든 데이터를 한꺼번에 생성하여 메모리에 저장하지 않고, 요청(next() 호출)이 있을 때마다 필요한 값만 생성하여 반환합니다.
  • 메모리 효율성: 특히 매우 크거나 무한한 시퀀스의 데이터를 다룰 때, 전체 데이터를 메모리에 로드할 필요가 없어 메모리 사용량을 크게 줄일 수 있습니다.
  • 상태 유지: yield 키워드를 만나 값을 반환한 후에도 함수의 실행 상태를 일시 중지하고 기억합니다. 다음 next() 호출 시 중지된 지점부터 다시 실행을 재개합니다.
  • 단일 순회: 한 번 생성된 값을 반환하면 해당 값은 다시 생성되지 않습니다. 즉, 한 번 순회하고 나면 제너레이터는 소진됩니다. (다시 사용하려면 제너레이터를 다시 생성해야 합니다.)

2. 왜 제너레이터가 필요할까요?

예를 들어, 1부터 10억까지의 숫자를 담은 리스트를 만든다고 상상해봅시다. 이 리스트는 엄청난 양의 메모리를 차지할 것입니다. 하지만 이 숫자들을 한 번만 순회하면서 어떤 작업을 한다면, 굳이 모든 숫자를 메모리에 저장할 필요가 있을까요? 제너레이터는 이런 경우에 각 숫자가 필요할 때마다 생성하여 메모리 부담을 줄여줍니다.


Part 2: yield 키워드 - 제너레이터 함수 만들기

일반 함수가 return 키워드를 사용하여 값을 반환하고 종료되는 반면, 제너레이터 함수yield 키워드를 사용하여 값을 반환합니다. yield는 값을 반환한 후에도 함수의 실행 상태를 일시 중지하고, 다음 호출 시 중지된 지점부터 실행을 재개합니다.

1. yield의 역할

  • 값 반환: yield 뒤에 오는 값을 호출자에게 반환합니다.
  • 상태 저장: 값을 반환한 후 함수의 현재 실행 상태(지역 변수 값, 실행 위치 등)를 저장합니다.
  • 실행 재개: 다음 next() 호출 시 저장된 상태부터 실행을 재개합니다.

2. return과의 차이점

  • return: 함수를 완전히 종료하고 값을 반환합니다.
  • yield: 값을 반환한 후 함수를 일시 중지하고, 다음 호출을 기다립니다.

예시: 간단한 숫자 시퀀스 제너레이터

Python
 
# 파일 이름: simple_generator.py

def my_simple_generator():
    print("제너레이터 시작")
    yield 1 # 첫 번째 값 반환
    print("첫 번째 yield 후")
    yield 2 # 두 번째 값 반환
    print("두 번째 yield 후")
    yield 3 # 세 번째 값 반환
    print("제너레이터 종료")

# 제너레이터 객체 생성 (함수를 호출해도 바로 실행되지 않음)
gen = my_simple_generator()
print(f"제너레이터 객체: {gen}")

print("\n--- next() 호출 시작 ---")
# next() 함수를 호출할 때마다 제너레이터가 실행됩니다.
value1 = next(gen)
print(f"next(gen) 결과: {value1}")

value2 = next(gen)
print(f"next(gen) 결과: {value2}")

value3 = next(gen)
print(f"next(gen) 결과: {value3}")

# 더 이상 yield 할 값이 없으면 StopIteration 예외 발생
try:
    value4 = next(gen)
    print(f"next(gen) 결과: {value4}")
except StopIteration:
    print("StopIteration 예외 발생: 더 이상 생성할 값이 없습니다.")

print("--- next() 호출 종료 ---")

 

[VS Code 터미널 출력]

파이썬 yield 키워드를 사용한 제너레이터 함수 예시

(제너레이터 객체의 메모리 주소(0x...)는 실행할 때마다 다를 수 있습니다.)


Part 3: 제너레이터 사용하기 - next()와 for 루프

제너레이터는 next() 함수를 통해 값을 하나씩 얻거나, for 반복문을 통해 모든 값을 순회할 수 있습니다.

1. for 반복문으로 제너레이터 순회

  • for 반복문은 내부적으로 next() 함수와 StopIteration 예외 처리를 자동으로 수행합니다. 따라서 제너레이터의 모든 값을 편리하게 순회할 수 있습니다.

예시: 피보나치 수열 제너레이터

Python
 
# 파일 이름: fibonacci_generator.py

def fibonacci_generator(n_max):
    """
    n_max까지의 피보나치 수열을 생성하는 제너레이터 함수.
    """
    a, b = 0, 1
    count = 0
    while count < n_max:
        yield a
        a, b = b, a + b
        count += 1
    print("피보나치 제너레이터 종료.")

print("--- 피보나치 수열 (처음 10개) ---")
# 제너레이터 객체 생성
fib_gen = fibonacci_generator(10)

# for 루프를 사용하여 제너레이터 순회
for num in fib_gen:
    print(num, end=" ") # 한 줄에 출력
print("\n")

print("--- 피보나치 수열 (처음 5개) ---")
# 제너레이터는 한 번 소진되면 다시 사용하려면 새로 생성해야 합니다.
fib_gen2 = fibonacci_generator(5)
fib_list = list(fib_gen2) # 제너레이터의 모든 값을 리스트로 변환
print(fib_list)

 

[VS Code 터미널 출력]

파이썬 제너레이터로 피보나치 수열 생성 예시

Part 4: 제너레이터 vs 리스트 - 언제 제너레이터를 사용할까?

제너레이터와 리스트는 모두 여러 데이터를 다루는 데 사용되지만, 내부 동작 방식과 적합한 사용 시기가 다릅니다.

특징 리스트 (List) 제너레이터 (Generator)
메모리 사용 모든 요소를 한꺼번에 메모리에 저장 (높음) 필요할 때마다 값을 생성 (낮음)
생성 시점 객체 생성 시 모든 요소 생성 next() 호출 시점에 요소 생성
접근 인덱스 접근 가능 (ex. my_list[0]) 순차적 접근만 가능 (next() 또는 for 루프)
재사용 여러 번 순회 가능 한 번 순회하면 소진됨 (다시 사용하려면 재생성)
적합한 경우 - 데이터 크기가 작을 때
- 인덱스 접근이 필요할 때
- 여러 번 순회해야 할 때
- 데이터 크기가 매우 크거나 무한할 때
- 데이터를 한 번만 순회할 때
- 메모리 사용량을 최적화해야 할 때
Sheets로 내보내기

1. 대량의 데이터 처리 시 메모리 비교

sys.getsizeof() 함수를 사용하여 객체의 메모리 크기를 대략적으로 확인할 수 있습니다.

Python
 
# 파일 이름: memory_comparison.py
import sys

# 1. 리스트로 100만 개의 숫자 생성
print("--- 리스트로 100만 개의 숫자 생성 ---")
large_list = [i for i in range(1_000_000)]
print(f"리스트의 메모리 크기: {sys.getsizeof(large_list)} 바이트")
# 각 숫자의 메모리까지 포함하면 훨씬 더 커집니다.

# 2. 제너레이터로 100만 개의 숫자 생성
print("\n--- 제너레이터로 100만 개의 숫자 생성 ---")
def large_number_generator(n):
    for i in range(n):
        yield i

large_gen = large_number_generator(1_000_000)
print(f"제너레이터 객체의 메모리 크기: {sys.getsizeof(large_gen)} 바이트")

# 제너레이터는 값을 요청할 때만 생성하므로, 객체 자체의 크기는 매우 작습니다.
# 리스트는 모든 값을 미리 저장하므로 훨씬 큽니다.

 

[VS Code 터미널 출력]

파이썬 제너레이터와 리스트 메모리 사용량 비교 예시

(메모리 크기는 파이썬 버전에 따라 약간 다를 수 있습니다. 중요한 것은 리스트가 제너레이터보다 훨씬 크다는 점입니다.)


마무리하며

이번 시간에는 파이썬의 강력한 고급 기능인 **제너레이터(Generator)**와 이를 만드는 핵심 키워드인 **yield**에 대해 알아보았습니다.

  • 제너레이터는 yield를 사용하여 값을 하나씩 '생성'하고, 함수의 상태를 유지하며 실행을 일시 중지/재개하는 특별한 함수입니다.
  • 가장 큰 장점은 메모리 효율성으로, 특히 대량의 데이터를 다루거나 무한한 시퀀스를 처리할 때 매우 유용합니다.
  • next() 함수나 for 반복문을 통해 제너레이터의 값을 순차적으로 얻을 수 있습니다.

제너레이터는 파이썬에서 고성능 및 메모리 최적화 코드를 작성하는 데 필수적인 도구이므로, 잘 익혀두면 실전에서 큰 도움이 될 것입니다.

다음 포스팅에서는 함수의 기능을 확장하거나 변경할 때 사용되는 파이썬의 또 다른 고급 문법인 **데코레이터(Decorator)**에 대해 알아보겠습니다.


궁금한 점이 있다면 언제든지 질문해주세요! 다음 포스팅에서 만나요!

반응형