ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 파이썬 제너레이터(Generator)를 이해한다는 것
    IT 2026. 1. 21. 15:12

    파이썬을 사용하다 보면 for 문은 자연스럽게 쓰게 되지만, yield라는 키워드는 꽤 오랫동안 낯설게 느껴지는경우가 많습니다. 제너레이터를 처음 접했을 때 많은 사람들이 이렇게 생각합니다.

    "리스트로 만들면 되는데, 왜 굳이 이렇게 복잡하게 써야 하지?"

    이 질문은 아주 정상적입니다. 사실 제너레이터는 문법부터 이해하려고 하면 오히려 더 어려워집니다. 제너레이터는 문법의 문제가 아니라, 데이터를 어떻게 다룰 것인가에 대한 관점의 변화에 가깝기 때문입니다.

     

    모든 데이터를 한 번에 가지고 있어야 할까?

    보통 우리는 어떤 데이터를 다룰 때, 먼저 리스트로 모두 만들어 놓고 그 다음에 처리하는 방식에 익숙합니다.

    def get_numbers():
        return [1, 2, 3, 4, 5]
    
    
    numbers = get_numbers()
    for n in numbers:
        print(n)

    이 코드는 아무 문제가 없어 보입니다. 하지만 만약 이 리스트가 수십만, 수백만 개의 데이터를 담고 있다면 이야기가 조금 달라집니다.

    실제로 필요한 것은 한 번에 모든 데이터가 아니라, 하나씩 순서대로 꺼내 쓰는 것일 수도 있기 때문입니다.

    바로 이 지점에서 제너레이터가 등장합니다.

     

    제너레이터는 "미리 만들어 두지 않는" 선택이다

    제너레이터의 핵심 아이디어는 아주 단순합니다.

    "지금 당장 필요한 값만 하나씩 만들어 쓰자"

    같은 동작을 제너레이터로 바꾸면 코드는 이렇게 달라집니다.

    def get_numbers():
    for i in range(1, 6):
        yield i
    
    
    numbers = get_numbers()
    for n in numbers:
        print(n)

    겉복에는 크게 차이가 없어 보이지만, 내부 동작 방식은 완전히 다릅니다. 리스트는 함수를 호출하는 순간 모든 값을 메모리에 올리지만, 제너레이터는 요청이 있을 때마다 다음 값을 하나씩 생성합니다.

    이 방식은 메모리 사용 측면에서 매우 큰 차이를 만들어냅니다.

     

    yield를 만나는 순간, 함수는 달라진다

    yield가 등장하는 순간, 파이썬에서 함수는 더 이상 일반 함수가 아닙니다. 그 함수는 값을 반환하고 끝나는 함수가 아니라, 실행 상태를 기억하며 다시 이어서 실행될 수 있는 함수가 됩니다.

    def simple_generator():
        print("start")
        yield 1
        print("middle")
        yield 2
        print("end")
    
    
    for value in simple_generator():
        print(value)

    이 코드를 실행해 보면, 함수가 처음부터 끝까지 한 번에 실행되지 않는다는 것을 확인할 수 있습니다. 제너레이터는 값을 하나 내보낼 때마다 잠시 멈췄다가, 다음 요청이 들어오면 그 지점부터 다시 실행됩니다.

    이 점에서 제너레이터는 마치 실행을 멈출 수 있는 함수처럼 보이기도 합니다.

     

    제너레이터는 상태를 가진 반복자다

    제너레이터를 조금 더 정확히 표현하면, 제너레이터는 상태를 기억하는 반복자(iterator) 입니다. 다음에 어떤 값을 내보낼지, 지금 어디까지 실행했을지를 스스로 알고 있습니다.

    gen = (i * i for i in range(5))
    
    
    print(next(gen))
    print(next(gen))
    print(next(gen))

    next를 호출할 때마다, 제너레이터는 이전 상태를 기억한 채 다음 값을 계산합니다. 이 과정에서 이미 지나간 값들은 다시 저장되지 않습니다.

     

    제너레이터 표현식은 왜 존재할까?

    리스트 컴프리헨션을 알고 있다면, 제너레이터 표현식은 아주 자연스럽게 받아들여질 수 있습니다.

    squares = [i * i for i in range(10)]
    
    gen_squares = (i * i for i in range(10))

    겉보기에는 대괄호와 소괄호의 차이뿐이지만, 의미는 전혀 다릅니다. 전자는 모든 값을 한 번에 만들고, 후자는 필요할 때마다 값을 만들어냅니다.

    "지금 당장 이 값들이 모두 필요한가?"

    이 질문에 그렇지 않다고 답할 수 있다면, 제너레이터 표현식은 아주 좋은 선택지가 됩니다.

     

    제너레이터는 언제 가장 빛날까?

    제너레이터는 다음과 같은 상황에서 특히 큰 장점을 가집니다.

    • 대용량 데이터 처리
    • 무한에 가까운 스트림 데이터
    • 파이프라인 형태의 데이터 처리
    def read_lines(file_path):
        with open(file_path) as f:
            for line in f:
                yield line

    이 함수는 파일 전체를 메모리에 올리지 않고, 한 줄 씩 읽어 처리할 수 있게 해줍니다. 이런 코드가 가능한 이유가 바로 제너레이터입니다.

     

    제너레이터와 리스트는 경쟁 관계가 아니다

    제너레이터를 설명할 때 종종 리스트와 비교하지만, 둘은 누가 더 좋은지를 따질 대상은 아닙니다. 제너레이터는 지연 평가(lazy evaluation) 가 필요할 때 사용하는 도구이고, 리스트는 즉시 모든 값이 필요할 때 사용하는 도구입니다.

    중요한 건 문법을 선택하는 것이 아니라, 상황에 맞는 사고를 선택하는 것입니다.

     

    클로저 / 데코레이터와 이어지는 이야기

    지금까지 살펴본 제너레이터 역시, 앞에서 다뤘던 클로저나 데코레이터와 같은 철학 위에 있습니다. 파이썬은 반복되는 작업, 불필요한 메모리 사용, 불분명한 책임 분리를 줄이기 위해 이런 기능들을 제공하고 있습니다.

    제너레이터는 그중에서도 "데이터를 다루는 방식"을 한 단계 더 우아하게 만들어주는 도구라고 볼 수 있습니다.

     

    마무리하며

    제너레이터는 처음에는 불필요하게 복잡해 보일 수 있습니다. 하지만 한 번 사고방식에 익숙해지면, 오히려 리스트를 무분별하게 만드는 것이 더 부담스럽게 느껴질 수도 있습니다.

    제너레이터를 이해한다는 것은 yield 문법을 외웠다는 뜻이 아니라, 이런 질문에 답할 수 있다는 의미입니다.

    "이 데이터는 정말 지금 전부 필요할까?"

    그 질문에 자연스럽게 제너레이터가 떠오른다면, 이미 제너레이터를 제대로 이해하고 있다고 볼 수 있습니다.

     

    이제 다음 블로그 글은 iterator와 yield from, 지연 평가에 대해서 이야기 해보도록 하겠습니다.

     

    감사합니다.

Designed by Tistory.