Python 제너레이터

발행: (2026년 2월 21일 오전 11:49 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

제너레이터 함수

제너레이터 함수는 lazy iterator를 반환하는 특수한 종류의 함수입니다.
이러한 객체들은 리스트처럼 반복할 수 있지만, 리스트와 달리 내용을 메모리에 저장하지 않습니다.

제너레이터 함수를 호출하면 generator object를 반환합니다.
함수 내부의 코드는 제너레이터의 next()(또는 __next__()) 메서드가 호출될 때만 실행되어 값을 한 번에 하나씩 생성합니다.

Source:

제너레이터 표현식 (제너레이터 컴프리헨션)

제너레이터 표현식은 리스트 컴프리헨션과 거의 동일하게 보이지만, 메모리 전체에 리스트를 만들지 않고 값을 지연(lazy)으로 생성하는 제너레이터 객체를 만듭니다.

리스트 컴프리헨션 vs. 제너레이터 표현식

특징리스트 컴프리헨션제너레이터 표현식
구문[x for x in ...](x for x in ...)
메모리 사용높음 – 모든 항목을 리스트에 저장낮음 – 반복자 상태만 저장
실행 방식즉시 – 모든 값을 한 번에 계산지연 – 필요할 때만 값 생성
결과 타입listgenerator

리스트 컴프리헨션 (메모리에 전체 리스트를 생성)

squares = [x * x for x in range(5)]
print(squares)
# Output: [0, 1, 4, 9, 16]

모든 값이 즉시 계산되어 메모리에 저장됩니다.

제너레이터 표현식 (지연 평가)

squares = (x * x for x in range(5))
print(squares)          # <generator object at 0x...>

아직 아무 것도 계산되지 않았습니다.
값은 제너레이터를 순회할 때만 생성됩니다.

제너레이터 표현식을 어떻게 사용하나요?

반복문으로 순회해야 합니다. 예:

for num in squares:
    print(num)

또는 리스트로 변환하여(평가를 강제) 사용할 수 있습니다:

print(list(squares))

메모리 사용 예시 (중요)

import sys

lst = [x for x in range(1_000_000)]
gen = (x for x in range(1_000_000))

print(sys.getsizeof(lst))   # large
print(sys.getsizeof(gen))   # small

제너레이터는 훨씬 적은 메모리를 사용합니다. 이는 모든 요소를 한 번에 저장하지 않기 때문입니다.

“함수 호출 없이”

보통은 함수를 사용해 제너레이터를 만들곤 합니다:

def my_generator():
    for x in range(5):
        yield x * x

제너레이터 표현식을 사용하면 함수 정의를 생략할 수 있습니다:

gen = (x * x for x in range(5))

매우 일반적인 사용 사례 – 함수에 직접 전달하기

많은 내장 함수가 모든 iterable을 받아들이므로, 제너레이터 표현식을 바로 전달할 수 있습니다:

total = sum(x * x for x in range(1_000_000))
  • 별도의 괄호가 필요 없음
  • 메모리 효율적
  • 깔끔한 문법

yield

yieldreturn과 유사하지만, 함수를 종료하는 대신 일시 중지하고 상태를 저장한 뒤 호출자에게 값을 반환합니다. 제너레이터가 다시 시작될 때(next()for 루프를 통해) 실행은 yield 바로 다음부터 계속됩니다.

핵심 포인트

  • 제너레이터의 지역 변수, 명령 포인터, 내부 스택이 저장됩니다.
  • 여러 개의 yield 문을 사용하여 값의 시퀀스를 생성할 수 있습니다.
  • return은 제너레이터를 완전히 종료시키며 StopIteration을 발생시킵니다.

제너레이터 표현식을 언제 사용해야 할까?

  • ✅ 대규모 데이터셋
  • ✅ 스트리밍 데이터
  • ✅ 단일 패스 반복
  • ✅ 메모리 민감 애플리케이션

피해야 할 경우:

  • ❌ 랜덤 인덱싱
  • ❌ 데이터에 대한 다중 패스

Source:

Lazy Evaluation 내부 작동 방식

Lazy evaluation은 값이 필요할 때만 계산된다는 의미입니다. 파이썬에서는 이터레이터제너레이터를 통해 이를 구현합니다.

Eager vs. Lazy (정신 모델)

Eager evaluation

data = [x * 2 for x in range(5)]
  • 루프가 즉시 실행되어 모든 값이 계산되고, 리스트가 메모리에 저장됩니다.

Lazy evaluation

data = (x * 2 for x in range(5))
  • 아직 아무것도 계산되지 않으며, 제너레이터 객체만 생성됩니다.
  • 값은 반복될 때마다 하나씩 생성됩니다.

제너레이터가 실제로 무엇인가

제너레이터는 본질적으로 상태 머신입니다:

  1. yield에서 실행을 일시 중지합니다.
  2. 현재 명령 포인터와 로컬 변수를 저장합니다.
  3. yield된 값을 반환합니다.
  4. 저장된 지점에서 다시 실행을 재개합니다.

단계별 실행 예시

def squares():
    for i in range(3):
        yield i * i
g = squares()   # 제너레이터가 생성되었으며 아직 코드가 실행되지 않음
next(g)          # → 0
next(g)          # → 1
next(g)          # → 4
next(g)          # StopIteration 발생

next(g) 호출은 다음 yield가 나올 때까지 실행을 재개하고, 그 후 다시 일시 중지합니다.

왜 제너레이터는 메모리를 효율적으로 사용하는가

  • range(1_000_000)start, stop, step만 저장합니다.
  • (x * x for x in range(1_000_000))은 range에 대한 참조, 현재 인덱스, 실행 상태를 저장합니다.

따라서 생성되는 항목 수와 관계없이 메모리 사용량은 일정하게 유지됩니다.

내장 함수에서의 지연 평가

FunctionLazy?
range()
map()
filter()
zip()
sum()❌ (consumes the iterator)

예시

m = map(lambda x: x * x, range(10))
# No computation occurs until `m` is iterated.

StopIteration이 지연 평가를 종료하는 방식

제너레이터가 끝에 도달하면 파이썬은 StopIteration을 발생시킵니다. 반복 프로토콜(예: for 루프)은 이 예외를 잡아 루프를 종료합니다.

gen = (x for x in range(3))
list(gen)   # [0, 1, 2]
list(gen)   # []  (generator is exhausted)

제너레이터는 단일 패스이며, 한 번 끝에 도달하면 새 제너레이터를 만들지 않는 한 다시 되감을 수 없습니다.

Source:

파이썬 제너레이터 – 고급 제어 메서드

제너레이터는 yield마다 실행을 일시 중지하면서 값을 게으르게(lazily) 생성합니다. 단순 반복을 넘어, 파이썬은 외부에서 제너레이터와 상호작용할 수 있는 세 가지 제어 메서드를 제공합니다:

메서드목적
next()제너레이터를 재개하고 다음 yield된 값을 반환
send(value)재개 마지막 yield 표현식의 결과가 되는 값을 전달
throw(exception)재개하고 제너레이터가 일시 중지된 지점에서 예외를 발생
close()제너레이터를 정상적으로 종료 (GeneratorExit을 내부에 발생)

1. .send(value) – 제너레이터에 데이터 보내기

보통 제너레이터는 값을 외부로만 yield합니다.
send()는 값을 제너레이터 안으로 다시 주입하며, 이는 가장 최근 yield 표현식의 반환값이 됩니다.

def counter():
    value = yield 0          # 첫 번째 yield
    while True:
        value = yield value + 1

gen = counter()

print(next(gen))       # 제너레이터 시작 → 0 반환
print(gen.send(10))    # 10을 보내고 → 11 반환
print(gen.send(20))    # 20을 보내고 → 21 반환

핵심 규칙

  • 첫 번째 호출은 반드시 next(gen) 또는 gen.send(None)이어야 합니다.
  • send(x)x를 마지막 yield 표현식에 할당합니다.

2. .throw(exception) – 제너레이터 내부에서 예외 발생시키기

throw()는 제너레이터가 일시 중지된 지점에 예외를 주입합니다.
제너레이터가 해당 예외를 잡으면 처리할 수 있고, 잡지 않으면 예외가 외부로 전파됩니다.

def generator():
    try:
        while True:
            yield "running"
    except ValueError:
        yield "ValueError handled"

gen = generator()
print(next(gen))                     # → running
print(gen.throw(ValueError))         # → ValueError handled

주요 활용 사례

  • 진행 중인 작업 취소
  • 오류 상황 신호 전달
  • 장시간 실행되는 제너레이터 중단

3. .close() – 제너레이터를 정상적으로 종료하기

close()를 호출하면 제너레이터 내부에 GeneratorExit이 발생하고, finally 블록에서 필요한 정리 작업을 수행할 수 있습니다.

def generator():
    try:
        while True:
            yield "working"
    finally:
        print("Cleaning up resources")

gen = generator()
print(next(gen))   # → working
gen.close()        # 정리 작업 트리거

출력

working
Cleaning up resources

라이프사이클 요약

메서드효과
next()제너레이터 재개
send(x)재개 + 값 전송 (x가 마지막 yield의 결과)
throw(e)재개 + 일시 중지 지점에서 예외(e) 발생
close()제너레이터 종료 (GeneratorExit 발생)

요약

Python의 generator는 다음과 같은 함수입니다:

  • yield 키워드를 사용합니다.
  • 값을 한 번에 하나씩 생성합니다.
  • 실행 사이에 로컬 상태를 기억합니다.

Python이 yield를 만나면:

  1. 값을 호출자에게 반환합니다.
  2. 실행을 일시 중지하고 현재 로컬 상태를 저장합니다.
  3. 다음 반복 시(또는 send, throw, close가 호출될 때) 정확히 그 지점부터 다시 시작합니다.
0 조회
Back to Blog

관련 글

더 보기 »

Python에서 숫자가 정수인지 확인하기

python def is_intx: int | float | str | None: ''' x가 정수 값을 나타내면 True를 반환하고, 그렇지 않으면 False를 반환합니다. 처리 대상: - int, float, 및 숫자 문자열 등....'''