[Python 시리즈 15편] 이터레이터와 제너레이터로 게으르게 계산하기

English version

대량 데이터를 다룰수록 "필요한 순간까지 계산을 미루는" 게으른 평가가 중요합니다. 용어가 세 개나 한꺼번에 등장하지만, 모두 "한 번에 한 항목씩 꺼내 쓰기"라는 같은 목표를 향합니다. 이번 글에서는 리스트처럼 익숙한 구조에서 출발해 점진적으로 이터레이터, 제너레이터로 확장합니다.

이번 글에서 새로 나오는 용어

  1. 이터러블: for 문으로 순회할 수 있는 모든 객체로 리스트·딕셔너리·문자열 등이 해당
  2. 이터레이터: next()를 호출해 다음 값을 돌려주는 객체로 __iter____next__를 구현함
  3. 제너레이터: yield 키워드를 써서 값을 하나씩 내보내는 함수 또는 표현식으로 자동으로 이터레이터가 됨
  4. 게으른 평가: 필요한 순간까지 계산을 미루어 메모리를 덜 쓰는 전략

개념 정리

학습 메모

  • 소요 시간: 60분
  • 준비물: 반복문·컴프리헨션 감각, 함수 정의 경험
  • 학습 목표: 직접 이터레이터 클래스를 만들고 제너레이터 함수·표현식으로 동일 로직을 구현해 비교하기
  • 이터러블은 순회 가능한 모든 객체입니다.
  • 이터레이터는 next()로 값을 내보내며 __iter____next__를 구현합니다.
  • 제너레이터는 yield 하나로 이터레이터를 만드는 함수 또는 표현식입니다.
  • 게으른 평가(lazy evaluation)는 필요한 순간까지 계산을 미루는 전략입니다.
  • 단계별로 Core → Optional 표기를 달았으니, 여유가 없다면 Core만 챙기고 넘어가도 됩니다.

코드로 이해하기

용어 정리 (Core)

  • 이터러블: for 문에 넣을 수 있는 객체. 내부적으로 __iter__를 구현하거나 __getitem__으로 순번 접근을 제공합니다. 예: 리스트, 딕셔너리, 문자열.
  • 이터레이터: next()를 호출해 다음 값을 돌려주는 객체. __next____iter__를 동시에 구현해야 합니다.
  • 제너레이터: yield 키워드로 값을 순차적으로 내보내는 특별한 함수 또는 표현식. 자동으로 이터레이터를 만듭니다.

직접 이터레이터 만들기 (Core)

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value


for number in Countdown(3):
    print(number)  # 3 2 1

StopIteration 예외가 발생하면 순회가 종료됩니다. 직접 이터레이터를 만들면 상태 제어가 가능하지만, 반복적인 템플릿 코드를 작성해야 한다는 단점이 있습니다.

제너레이터 함수와 yield (Core)

클래스보다 간단한 예시부터 만져보면 yield가 낯설지 않습니다. "값을 건네주고 잠깐 멈춘다"는 느낌으로 이해하세요.

def countdown(start):
    current = start
    while current > 0:
        yield current
        current -= 1


for number in countdown(3):
    print(number)

yield를 사용하는 순간 함수는 제너레이터가 됩니다. 실행을 멈추고 값을 내보낸 뒤, 다음 next() 호출이 올 때 다시 이어서 수행합니다. 별도의 클래스를 만들지 않아도 상태를 자연스럽게 유지할 수 있습니다.

제너레이터 표현식 (Core → Plus)

squares = (n * n for n in range(1, 1_000_001))
first_ten = list(itertools.islice(squares, 10))

앞 편에서 잠깐 언급한 둥근 괄호 버전이 제너레이터 표현식입니다. list(...)처럼 소비하기 전까지 메모리를 거의 사용하지 않습니다.

이터레이터 도구 상자 (Optional)

itertools 모듈은 이터레이터를 조합하기 위한 표준 도구입니다.

  • itertools.count(start=0, step=1): 무한 증가 시퀀스
  • itertools.cycle(iterable): 주어진 시퀀스를 반복 순환
  • itertools.chain(a, b, ...): 여러 이터러블을 이어 붙임
  • itertools.groupby(iterable, key): 정렬된 데이터를 그룹화

메모리와 성능 전략 (Optional)

  • 대량 CSV를 처리할 때는 전체를 리스트로 읽지 말고 제너레이터로 한 줄씩 다루세요.
  • 네트워크 응답처럼 느린 소스는 제너레이터로 묶으면 백프레셔(consumer 쪽 속도 조절)를 자연스럽게 구현할 수 있습니다.
  • 필요한 항목 수가 작다면 itertools.islice로 앞부분만 잘라 쓰면 됩니다.

왜 중요할까

  • 이터러블은 순회가 가능한 모든 객체이고 이터레이터는 next()로 값을 내보냅니다.
  • 제너레이터 함수는 상태를 기억하며 한 값씩 내보냅니다.
  • itertools와 제너레이터 표현식을 활용하면 대용량에서도 메모리 낭비 없이 파이프라인을 구성할 수 있습니다.

실습

  • 따라 하기: Countdown 클래스와 countdown 제너레이터 함수를 모두 작성해 같은 결과가 나오는지 비교합니다.
  • 확장하기: 대용량 CSV를 흉내 낸 리스트를 만들고 제너레이터 표현식과 itertools.islice로 필요한 일부만 처리합니다.
  • 디버깅: StopIterationreturn으로 바꿔 의도치 않게 None이 나오는 상황을 만든 뒤 올바른 예외 처리로 수정합니다.
  • 완료 기준: 클래스 기반 이터레이터, yield 함수, 제너레이터 표현식 세 가지를 한 노트북에서 실행하며 언제 어떤 것을 고를지 설명할 수 있을 때입니다.

마무리

다음 글에서는 파일·네트워크 리소스를 안전하게 다루는 컨텍스트 매니저with 문을 다룹니다.

💬 댓글

이 글에 대한 의견을 남겨주세요