ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Iterable ? Iterator? Generator?
    python 2026. 1. 9. 03:53

    파이썬을 공부하다 보면 for 문을 수도 없이 사용하게 됩니다. 하지만 Iterable, Iterator, Generator라는 용어가 나오면 갑자기 머리가 복잡해지죠.

    "리스트도 반복되고, 제너레이터도 반복되는데 도대체 무슨 차이지?"

    오늘은 이 세 가지 개념의 정확한 정의와 포함 관계(상속 관계)를 코드로 확실하게 정리해 보겠습니다.


    한 눈에 보는 포함 관계 (The Big Picture)

    결론부터 말하면, 이들은 별개의 개념이 아니라 수학의 부분집합(Subset) 혹은 상속(Inheritance) 관계를 가집니다.

    Iterable ⊃ Iterator ⊃ Generator

    (이터러블이 가장 크고, 제너레이터가 가장 작습니다.)

    • Iterable: 반복 가능한 모든 것 (가장 큰 범위)
    • Iterator: 이터러블 중에서 next()로 값을 꺼낼 수 있는 것
    • Generator: 이터레이터를 아주 쉽게 만들 수 있게 해주는 특수한 문법

    이터러블 (Iterable)

    • 정의: "멤버를 하나씩 차례로 반환할 수 있는 객체"
    • 특징: for 문을 돌릴 수 있으면 모두 이터러블입니다.
    • 종류: list, tuple, str, dict, set 등 우리가 아는 대부분의 자료형.
    • 기술적 조건: 내부적으로 __iter__ 메소드를 가지고 있어야 합니다.
    # 리스트는 이터러블입니다.
    my_list = [1, 2, 3]
    
    for i in my_list:
        print(i)
    
    # 하지만 next()는 쓸 수 없습니다. (아직 이터레이터가 아니니까요)
    # next(my_list)  # TypeError 발생!

    비유: '책(Book)'. 읽을 수는 있지만, 내가 어디까지 읽었는지 스스로 기억하지는 못합니다.

     

    이터레이터 (Iterator)

    • 정의: "데이터의 스트림을 표현하는 객체"
    • 특징: next() 함수를 호출할 때마다 다음 값을 리턴합니다.
    • 기술적 조건: __iter__와 __next__ 메소드를 모두 가지고 있어야 합니다.
    • 만드는 법: 이터러블에 iter() 함수를 씌우면 이터레이터가 됩니다.
    my_list = [1, 2, 3]
    my_iter = iter(my_list)  # 리스트를 이터레이터로 변환
    
    print(type(my_iter))  # <class 'list_iterator'>
    
    print(next(my_iter))  # 1
    print(next(my_iter))  # 2
    print(next(my_iter))  # 3
    # print(next(my_iter)) # StopIteration 에러 발생 (더 이상 값이 없음)

    비유: '북마크(Bookmark)'. 책(이터러블)에 꽂힌 북마크는 내가 어디를 읽을 차례인지 정확히 알고 있습니다.

     

    제너레이터 (Generator)

    • 정의: "이터레이터를 생성하는 아주 간편하고 강력한 도구"
    • 특징: 함수 안에 return 대신 yield를 사용하거나, (x for x in data) 형태의 표현식을 사용합니다.
    • 핵심: 모든 제너레이터는 이터레이터입니다. 따라서 next()를 쓸 수 있습니다.
    • 장점: Lazy Evaluation(지연 평가). 데이터를 미리 메모리에 다 만들어두지 않고, 필요할 때마다 하나씩 생성하므로 메모리 효율이 압도적입니다.
    # 1. 제너레이터 함수 (yield 사용)
    def my_gen_func():
        yield 1
        yield 2
        yield 3
    
    # 2. 제너레이터 표현식 (괄호 사용)
    my_gen_exp = (x for x in [1, 2, 3])
    
    print(next(my_gen_exp)) # 1

    비유: '소설가'. 책처럼 이미 쓰여진 내용을 읽는 게 아니라, 독자가 "다음 내용 뭐야?"(next)라고 물어볼 때마다 즉석에서 다음 문장을 써서 건네줍니다.

    코드로 증명하는 상속 관계 (팩트 체크)

    파이썬의 추상 베이스 클래스(collections.abc)를 사용하면 이 족보를 눈으로 확인할 수 있습니다.

    from collections.abc import Iterable, Iterator, Generator
    
    # 제너레이터 생성
    gen = (x for x in range(3))
    
    # 1. 제너레이터는 이터레이터인가? -> YES
    print(isinstance(gen, Iterator))  # True
    
    # 2. 제너레이터는 이터러블인가? -> YES
    print(isinstance(gen, Iterable))  # True
    
    # 3. 리스트는 이터레이터인가? -> NO (이터러블일 뿐)
    print(isinstance([1, 2, 3], Iterator)) # False

    함수로 보는 Iterable의 위력

    우리는 흔히 합계를 구할 때 sum([1, 2, 3])처럼 리스트를 넣습니다. 하지만 파이썬 공식 문서를 보면 sum()의 정의는 다음과 같습니다.

    sum(iterable, start=0)

    매개변수 이름이 list가 아니라 iterable입니다. 이게 무슨 뜻일까요?

    "무엇이든 반복만 가능하면 된다" (다형성)

    sum() 함수는 들어온 데이터가 리스트인지, 튜플인지, 세트인지, 아니면 제너레이터인지 신경 쓰지 않습니다. 그저 "하나씩 꺼낼 수 있는(Iterable) 녀석인가?"만 확인합니다.

    내부적으로 sum()은 이렇게 동작한다고 상상해볼 수 있습니다. (Psudo 코드)

    # Psudo Code 
    def my_sum(iterable):
        total = 0
        # 들어온 게 리스트든 제너레이터든 상관없이 반복문(for)만 돌릴 수 있으면 OK
        for x in iterable: 
            total += x
        return total

    이 덕분에 우리는 다양한 자료형을 sum()에 넣을 수 있습니다.

    • sum([1, 2, 3]) (리스트: 이터러블 O)
    • sum((1, 2, 3)) (튜플: 이터러블 O)
    • sum({1, 2, 3}) (세트: 이터러블 O)
    • sum(range(1, 4)) (Range: 이터러블 O)
    • sum(x for x in range(1, 4)) (제너레이터: 이터러블 O)

     

    리스트 vs 제너레이터: 메모리 사용량의 결정적 차이

    이 개념이 가장 빛을 발하는 순간이 바로 제너레이터를 sum()에 넣었을 때입니다.

    만약 1억 개의 숫자를 더해야 한다면 어떻게 될까요?

    • A. 리스트를 사용하는 경우 (비효율적)
    # [1, 2, ..., 100000000] 리스트를 메모리에 통째로 만듦
    # 메모리 폭발 가능성 있음 💥
    result = sum([i for i in range(100000000)]) 
    • 제너레이터를 사용하는 경우 (효율적)
    # 숫자를 미리 만들지 않음.
    # sum()이 "다음 숫자 줘" 할 때마다 하나씩 생성해서 더하고 버림.
    # 메모리는 숫자 1개 분량만 필요
    result = sum(i for i in range(100000000))

    sum()이 이터러블을 인자로 받도록 설계되었기 때문에, 거대한 리스트를 만들지 않고도 스트림(Stream) 방식으로 데이터를 흘려보내며 계산할 수 있는 것입니다.

     

    Iterator를 쓰면 이렇게 "하나씩 꺼내준다"는 점에서는 메모리를절약하는데,  하지만 메모리를 아끼느냐(Lazy Evaluation)"에 대해서는 원본이 무엇이냐에 따라 다릅니다.

    # 1. 거대한 리스트 생성 (이미 메모리에 1억 개 데이터가 꽉 참 💥)
    big_list = [1, 2, 3, ... 1억] 
    
    # 이터레이터로 변환
    my_iter = iter(big_list)
    
    #  하나씩 꺼냄
    print(next(my_iter))
    
    
    
    # 2. 제너레이터 생성 (데이터 생성 안 함, 규칙만 기억함 🍃)
    my_gen = (x for x in range(100000000))
    
    # 하나씩 꺼냄 (이때 데이터를 생성!)
    print(sys.getsizeof(next(my_gen))) #28 byte int객체크기

    리스트를 Iterator로 변환을 해도, 이미 1억개의 거대 메모리에서 하나씩 꺼내는 겁니다. 반면 my_gen은 next 시에 그때 데이타를 가져옵니다.

     

     


    핵심 요약 (Cheat Sheet)

    구분 Iterable (이터러블) Iterator (이터레이터) Generator (제너레이터)
    대표 예시 리스트, 튜플, 문자열 iter(리스트) 반환값 yield 함수, (...) 표현식
    for 문 사용 O O O
    next() 사용 X O O
    메모리 효율 데이터 크기만큼 차지 상태만 기억하므로 작음 최상 (필요할 때 생성)
    포함 관계 할아버지 아버지 자식

     

     

    결론

    • Iterable: 반복할 수 있는 재료 (리스트 등)
    • Iterator: 재료를 하나씩 꺼내주는 도구 (next() 보유)
    • Generator: 그 도구를 아주 쉽게 만드는 방법 (yield 사용)

    우리가 평소에 쓰는 for i in list: 구문은 사실 파이썬 내부적으로 list(이터러블)를 iterator로 변환한 뒤, next()를 계속 호출하다가 StopIteration 에러가 나면 멈추는 방식으로 동작합니다.

    이 관계를 이해하면 대용량 데이터를 처리할 때 왜 리스트 대신 제너레이터를 써야 하는지(Memory Efficiency) 명확히 알 수 있습니다.

Designed by Tistory.