Skip to main content

제너레이터와 이터레이터

학습 목표

  • 이터러블(Iterable)과 이터레이터(Iterator)의 차이를 이해한다
  • yield로 제너레이터 함수를 작성할 수 있다
  • 제너레이터 표현식을 사용할 수 있다
  • 메모리 효율적인 데이터 처리 패턴을 적용할 수 있다

왜 중요한가

제너레이터는 데이터를 한 번에 메모리에 올리지 않고, 필요할 때 하나씩 생성하는 지연 평가(Lazy Evaluation) 방식을 구현합니다. ML/DL에서 대용량 데이터셋을 메모리 효율적으로 처리하는 DataLoader가 바로 제너레이터 패턴입니다.

이터레이터 프로토콜

# 이터러블: __iter__() 메서드를 가진 객체
# 이터레이터: __iter__()와 __next__() 메서드를 가진 객체

numbers = [1, 2, 3]          # 이터러블
iterator = iter(numbers)     # 이터레이터 생성

print(next(iterator))        # 1
print(next(iterator))        # 2
print(next(iterator))        # 3
# print(next(iterator))      # StopIteration 예외!

# for 문은 내부적으로 이터레이터 프로토콜 사용
for num in [1, 2, 3]:
    print(num)
# 위 코드는 아래와 동일
it = iter([1, 2, 3])
while True:
    try:
        num = next(it)
        print(num)
    except StopIteration:
        break

제너레이터 함수

yield 키워드를 사용하면 함수가 제너레이터가 됩니다.
def count_up(n):
    """0부터 n-1까지 하나씩 반환합니다."""
    i = 0
    while i < n:
        yield i        # 값을 반환하고 일시 중지
        i += 1         # 다음 next() 호출 시 여기서 재개

gen = count_up(5)
print(type(gen))       # <class 'generator'>

print(next(gen))       # 0
print(next(gen))       # 1
print(next(gen))       # 2

# for 문으로 순회
for num in count_up(5):
    print(num, end=" ")  # 0 1 2 3 4

return vs yield

# return: 모든 데이터를 한 번에 메모리에 저장
def get_squares_list(n):
    result = []
    for i in range(n):
        result.append(i ** 2)
    return result

# yield: 필요할 때만 하나씩 생성
def get_squares_gen(n):
    for i in range(n):
        yield i ** 2

# 메모리 비교
import sys
list_result = get_squares_list(1_000_000)
gen_result = get_squares_gen(1_000_000)

print(sys.getsizeof(list_result))  # ~8MB
print(sys.getsizeof(gen_result))   # ~200 bytes!

제너레이터 표현식

# 리스트 컴프리헨션 -> 리스트
squares_list = [x ** 2 for x in range(1000)]

# 제너레이터 표현식 -> 제너레이터
squares_gen = (x ** 2 for x in range(1000))

# sum, min, max 등에 직접 전달
total = sum(x ** 2 for x in range(100))
print(total)  # 328350

yield from

다른 제너레이터에게 위임할 때 사용합니다.
def gen_a():
    yield 1
    yield 2

def gen_b():
    yield 3
    yield 4

# yield from 없이
def combined_manual():
    for item in gen_a():
        yield item
    for item in gen_b():
        yield item

# yield from 사용 (간결)
def combined():
    yield from gen_a()
    yield from gen_b()

print(list(combined()))  # [1, 2, 3, 4]

# 재귀적 활용 - 트리 순회
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

data = [1, [2, [3, 4], 5], 6]
print(list(flatten(data)))  # [1, 2, 3, 4, 5, 6]

커스텀 이터레이터 클래스

class Countdown:
    """카운트다운 이터레이터"""
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return self

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

for num in Countdown(5):
    print(num, end=" ")  # 5 4 3 2 1

AI/ML에서의 활용

# 데이터 로더 패턴 (PyTorch DataLoader의 원리)
def batch_generator(data, batch_size=32):
    """데이터를 배치 단위로 반환하는 제너레이터"""
    for i in range(0, len(data), batch_size):
        yield data[i:i + batch_size]

# 1000개 데이터를 32개씩 배치
data = list(range(1000))
for batch_idx, batch in enumerate(batch_generator(data, 32)):
    if batch_idx < 3:
        print(f"배치 {batch_idx}: {len(batch)}개 샘플")

# 대용량 파일 스트리밍
def read_large_file(filepath, chunk_size=1024):
    """대용량 파일을 청크 단위로 읽는 제너레이터"""
    with open(filepath, "r") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

# 무한 데이터 증강
import random

def augmented_data(dataset, num_augmentations=3):
    """원본 데이터에 증강 데이터를 추가하여 무한 반환"""
    while True:
        for sample in dataset:
            yield sample  # 원본
            for _ in range(num_augmentations):
                # 노이즈 추가, 변환 등
                augmented = sample + random.gauss(0, 0.1)
                yield augmented

# 학습 루프에서 사용
# for step, sample in enumerate(islice(augmented_data(train_data), 10000)):
#     train_step(sample)
네. 제너레이터는 한 번 소진되면 재사용할 수 없습니다. 다시 순회하려면 제너레이터를 새로 생성해야 합니다.
send(value)는 yield 표현식에 값을 전달하고, throw(exception)은 제너레이터 내부에서 예외를 발생시킵니다. 코루틴(Coroutine) 패턴에서 사용되지만, 일반적인 용도에서는 거의 필요하지 않습니다.

체크리스트

  • 이터러블과 이터레이터의 차이를 설명할 수 있다
  • yield로 제너레이터 함수를 작성할 수 있다
  • 제너레이터의 메모리 효율 이점을 설명할 수 있다
  • yield from으로 서브제너레이터에 위임할 수 있다

다음 문서