Generator와 yield — 메모리를 안 먹는 이터레이션의 원리
yield가 함수 실행을 "일시 정지"시키는 메커니즘
리스트를 만들면 전체를 메모리에 올린다. 1억 개 항목이면 1억 개 분량의 메모리를 쓴다.
# 메모리를 전부 먹는다
numbers = [x * 2 for x in range(100_000_000)]
# 메모리를 거의 안 먹는다
numbers = (x * 2 for x in range(100_000_000))
두 번째는 generator expression이다. 값을 미리 안 만들고, next()가 호출될 때마다 하나씩 계산한다.
yield가 뭘 하는가
일반 함수는 return하면 스택 프레임이 사라진다. 함수의 로컬 변수, 실행 위치 — 전부 없어진다.
yield는 다르다. 값을 반환하면서 스택 프레임을 보존한다. 다음에 next()가 호출되면 yield 다음 줄부터 재개한다. 로컬 변수도 그대로.
def counter():
n = 0
while True:
yield n # 여기서 멈추고 n을 반환
n += 1 # 다음 next()에서 여기부터 재개
gen = counter()
print(next(gen)) # 0
print(next(gen)) # 1 — n이 살아 있다
CPython 내부
CPython에서 generator는 PyGenObject로 구현된다. gi_frame 필드에 프레임 객체가 보존되고, gi_code에 바이트코드가 저장된다. next()를 호출하면 _PyEval_EvalFrameDefault가 보존된 프레임에서 실행을 재개한다.
일반 함수 호출과 달리 새 프레임을 안 만든다 — 기존 프레임을 그대로 쓴다. 이게 generator가 가벼운 이유.
send()와 양방향 통신
gen.send(value)로 yield 지점에 값을 주입할 수 있다. 이게 coroutine의 기반이다. asyncio의 async/await도 내부적으로 generator + send()에서 진화한 것이다.
핵심 포인트
yield는 값을 반환하면서 함수의 스택 프레임을 보존한다
next() 호출 시 yield 다음 줄부터 재개 — 로컬 변수 유지
CPython에서 PyGenObject.gi_frame에 프레임이 보존됨
send()로 yield 지점에 값을 주입 가능 — coroutine의 기반