Python 제너레이터
Source: Dev.to
제너레이터 함수
제너레이터 함수는 lazy iterator를 반환하는 특수한 종류의 함수입니다.
이러한 객체들은 리스트처럼 반복할 수 있지만, 리스트와 달리 내용을 메모리에 저장하지 않습니다.
제너레이터 함수를 호출하면 generator object를 반환합니다.
함수 내부의 코드는 제너레이터의 next()(또는 __next__()) 메서드가 호출될 때만 실행되어 값을 한 번에 하나씩 생성합니다.
Source: …
제너레이터 표현식 (제너레이터 컴프리헨션)
제너레이터 표현식은 리스트 컴프리헨션과 거의 동일하게 보이지만, 메모리 전체에 리스트를 만들지 않고 값을 지연(lazy)으로 생성하는 제너레이터 객체를 만듭니다.
리스트 컴프리헨션 vs. 제너레이터 표현식
| 특징 | 리스트 컴프리헨션 | 제너레이터 표현식 |
|---|---|---|
| 구문 | [x for x in ...] | (x for x in ...) |
| 메모리 사용 | 높음 – 모든 항목을 리스트에 저장 | 낮음 – 반복자 상태만 저장 |
| 실행 방식 | 즉시 – 모든 값을 한 번에 계산 | 지연 – 필요할 때만 값 생성 |
| 결과 타입 | list | generator |
리스트 컴프리헨션 (메모리에 전체 리스트를 생성)
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 문
yield는 return과 유사하지만, 함수를 종료하는 대신 일시 중지하고 상태를 저장한 뒤 호출자에게 값을 반환합니다. 제너레이터가 다시 시작될 때(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))
- 아직 아무것도 계산되지 않으며, 제너레이터 객체만 생성됩니다.
- 값은 반복될 때마다 하나씩 생성됩니다.
제너레이터가 실제로 무엇인가
제너레이터는 본질적으로 상태 머신입니다:
- 각
yield에서 실행을 일시 중지합니다. - 현재 명령 포인터와 로컬 변수를 저장합니다.
yield된 값을 반환합니다.- 저장된 지점에서 다시 실행을 재개합니다.
단계별 실행 예시
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에 대한 참조, 현재 인덱스, 실행 상태를 저장합니다.
따라서 생성되는 항목 수와 관계없이 메모리 사용량은 일정하게 유지됩니다.
내장 함수에서의 지연 평가
| Function | Lazy? |
|---|---|
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를 만나면:
- 값을 호출자에게 반환합니다.
- 실행을 일시 중지하고 현재 로컬 상태를 저장합니다.
- 다음 반복 시(또는
send,throw,close가 호출될 때) 정확히 그 지점부터 다시 시작합니다.