🔄

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()에서 진화한 것이다.

핵심 포인트

1

yield는 값을 반환하면서 함수의 스택 프레임을 보존한다

2

next() 호출 시 yield 다음 줄부터 재개 — 로컬 변수 유지

3

CPython에서 PyGenObject.gi_frame에 프레임이 보존됨

4

send()로 yield 지점에 값을 주입 가능 — coroutine의 기반

사용 사례

대용량 파일 처리 — 한 줄씩 읽으면서 메모리 사용량 일정 유지 무한 시퀀스 — counter, fibonacci 같은 끝없는 이터레이터