Menu

파이썬 제너레이터 완벽 정리: yield와 지연 평가

파이썬 제너레이터가 값을 하나씩 lazy하게 만들어내는 원리부터 yield 키워드, 제너레이터 표현식, 그리고 리스트 대신 써야 할 상황까지 정리했습니다.

잠시 멈출 수 있는 함수

제너레이터(generator)는 일반 함수처럼 생겼지만, 결과 전체를 한 번에 계산해서 반환하지 않습니다. 대신 값을 하나씩 yield(양보)하면서, 호출한 쪽에서 다음 값을 달라고 할 때까지 그 자리에서 잠시 멈춰 있습니다.

가장 단순한 예시를 보시죠:

main.py
Output
Click Run to see the output here.

return 대신 yield가 쓰인 걸 눈여겨보세요. for가 처음으로 값을 요청하면, 파이썬은 함수 본문을 실행하다가 yield 1을 만나는 순간 바로 거기서 멈춥니다. 그리고 1을 루프에 넘겨준 뒤, 변수 상태까지 포함해서 어디서 멈췄는지 통째로 기억해둬요. 다음 반복에서는 멈췄던 그 자리부터 다시 이어집니다. current += 1을 실행하고, while로 돌아가서 yield 2를 내놓죠. 이런 식으로 루프 조건이 거짓이 될 때까지 계속되다가, 조건이 깨지면 제너레이터는 그냥 끝납니다.

이 '멈췄다 이어가기'가 제너레이터의 핵심입니다.

그냥 리스트로 만들면 안 되나요?

리스트로 만들어 버리면 모든 값을 미리 한꺼번에 메모리에 올려야 하기 때문이에요:

main.py
Output
Click Run to see the output here.

5개 정도라면 뭐, 상관없죠. 그런데 정수 5천만 개가 필요한데 그중 어떤 조건에 맞는 첫 번째 값 하나만 쓰고 싶다고 해봅시다. 리스트 버전은 5천만 개를 전부 메모리에 올려놓고 나서 대부분을 버리게 됩니다. 반면 제너레이터 버전은 호출한 쪽에서 요청한 만큼만 정확히 만들어냅니다. for 루프가 원하는 값을 찾아서 break를 걸면 제너레이터도 거기서 그냥 멈춰버리죠.

이게 꼭 몸에 배게 해둘 만한 패턴입니다: 제너레이터를 쓰면 결과를 얼마나 소비할지 미리 정하지 않고도 반복 코드를 작성할 수 있다.

제너레이터 표현식 (Generator Expression)

리스트 컴프리헨션을 써봤다면 문법은 이미 다 아는 거나 마찬가지입니다. 대괄호를 소괄호로 바꾸기만 하면 돼요:

main.py
Output
Click Run to see the output here.

squares_gen 자체는 아직 아무것도 계산하지 않습니다. 그냥 레시피일 뿐이죠. 반복을 돌려야 비로소 레시피가 한 단계씩 실행됩니다.

제너레이터 표현식은 이터러블을 받아서 처리하는 함수에 인자로 넘길 때 진가를 발휘합니다:

main.py
Output
Click Run to see the output here.

중간 리스트를 만들지 않습니다. sum, max, any 같은 함수는 값을 하나씩 꺼내 쓰기 때문에 제너레이터와 궁합이 딱 맞습니다.

대용량 파일을 한 줄씩 읽기

제너레이터가 실전에서 가장 빛을 발하는 순간이 바로 이 경우입니다. 메모리에 한 번에 올릴 수 없을 만큼 큰 파일을 처리해야 할 때죠:

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

파일은 지연 방식으로 읽힙니다. 제너레이터를 호출할 때마다 디스크에서 한 줄씩 읽고, 필터링을 거쳐 값을 yield 합니다. 덕분에 파일 크기가 아무리 커져도 메모리 사용량은 일정하게 유지됩니다.

한 번 쓰면 끝

제너레이터는 한 번만 순회할 수 있습니다. 끝까지 돌고 나면 그대로 소진되어 버립니다:

main.py
Output
Click Run to see the output here.

두 번째 반복문은 아무것도 출력하지 않습니다. 제너레이터가 이미 소진됐기 때문이죠.

여러 번 순회해야 한다면, 제너레이터 함수를 다시 호출해서 새 제너레이터를 만들거나, list(...)로 시퀀스를 한 번에 받아두고 그 리스트를 반복해서 쓰면 됩니다. 선택 기준은 비용입니다. 값을 다시 만들어내는 게 가볍다면 매번 새로 만드는 쪽이 낫고, 시퀀스가 작다면 리스트에 담아두는 편이 편합니다.

next()로 수동 순회하기

for 문을 써야 하는 건 아닙니다. next()를 쓰면 값을 하나씩 꺼낼 수 있습니다:

main.py
Output
Click Run to see the output here.

StopIteration은 제너레이터가 "이제 끝났어요"라고 알리는 신호입니다. for 루프는 이 예외를 조용히 처리해 주고요. 직접 코드를 다룰 때는 next(gen, default)처럼 기본값을 넘겨 주면 예외를 피할 수 있습니다.

무한 제너레이터

값이 필요할 때마다 하나씩 만들어지는 구조이기 때문에, 제너레이터는 끝이 없는 시퀀스도 표현할 수 있습니다. 소비하는 쪽에서 더 이상 요청하지 않으면 그만이니까요:

main.py
Output
Click Run to see the output here.

while True 안에 yield가 있다고 해서 프로그램이 멈추는 게 아닙니다. "계속 달라고 하면 계속 만들어주겠다"는 뜻일 뿐이죠. 언제 멈출지는 소비자 쪽에서 정합니다.

이 패턴은 스트리밍 데이터나 이벤트 루프, 그리고 길이가 정해져 있지 않은 소스에서 값을 꺼내 쓰는 모든 곳에서 자주 등장합니다.

yield from: 다른 이터러블에 위임하기

제너레이터에서 다른 이터러블의 값을 전부 내보내고 싶다면, yield from 한 줄이면 충분합니다:

main.py
Output
Click Run to see the output here.

yield from 없이 똑같이 쓰려면 중첩 for 문 안에 yield x를 넣어야 하죠. 덤으로 send()throw() 호출도 올바르게 전달해 주긴 하는데, 평소에 쓸 때는 그냥 "이 녀석에서 나오는 값을 전부 yield 한다"로 이해하면 충분합니다.

제너레이터를 써야 할 때

제너레이터가 정답이라는 신호는 보통 세 가지입니다:

  1. 시퀀스가 크거나, 무한할 수도 있거나, 전부 만들어 두기엔 비용이 큽니다.
  2. 소비하는 쪽이 중간에 멈출 수도 있습니다(예를 들어 첫 일치 항목에서 break 하는 경우).
  3. 중간 리스트를 만들지 않고 filter, map, take 같은 변환을 체인으로 이어 쓰고 싶습니다.

반대로 쓰지 말아야 할 때도 있습니다:

  • 임의 접근(seq[42])이 필요합니다. 제너레이터는 앞으로만 갑니다.
  • 같은 시퀀스를 여러 번 순회해야 합니다. 이럴 땐 리스트를 쓰세요.
  • 시퀀스가 작고, 이미 손에 들고 있습니다. 리스트 컴프리헨션이 더 간단합니다.

제너레이터, 리스트 컴프리헨션, 그냥 리스트는 각각 다른 상황에서 정답이 됩니다. 관건은 고민 없이 바로 고를 수 있느냐인데, 이 감각을 가장 빨리 기르는 방법은 반복문을 쓸 때마다 "일단 전부 만들어 둘까, 아니면 하나씩 만들까" 중 어느 쪽이 더 맞는지 한 번씩 짚어 보는 겁니다.

다음 주제: 컨텍스트 매니저 파헤치기

여기까지 해서 파이썬이 반복(iteration)에 사용하는 관용구는 거의 다 훑어봤습니다. 다음은 컨텍스트 매니저, 즉 with 문입니다. 파일이나 네트워크 연결에서 데이터를 스트리밍할 때 제너레이터와 찰떡궁합이죠.

자주 묻는 질문

파이썬에서 제너레이터(generator)가 뭔가요?

제너레이터는 값을 한 번에 하나씩 내보내면서 중간중간 실행이 멈추는 함수입니다. 일반 함수처럼 def로 정의하지만 return 대신 yield를 씁니다. 호출하면 바로 실행되지 않고 제너레이터 객체가 반환되며, for문을 돌거나 next()를 호출할 때마다 다음 yield를 만날 때까지 실행됩니다.

리스트와 제너레이터는 뭐가 다른가요?

리스트는 모든 원소를 한꺼번에 메모리에 담아둡니다. 반면 제너레이터는 필요할 때마다 값을 계산하고, 소비된 값은 그대로 버립니다. 데이터가 크거나 무한한 시퀀스를 다룰 때는 제너레이터가 거의 일정한 작은 메모리만 쓰지만, 결과를 여러 번 재사용해야 하는 작은 데이터라면 리스트가 유리합니다.

제너레이터를 두 번 반복할 수 있나요?

아니요. 한 번 끝까지 돌고 나면 제너레이터는 소진된 상태가 되어서, 같은 객체로 다시 for문을 돌려도 아무것도 나오지 않습니다. 여러 번 순회해야 한다면 제너레이터 함수를 다시 호출해 새 객체를 만들거나, 한 번 돌려서 결과를 리스트로 저장해두세요.

Coddy로 코딩 배우기

시작하기