이터레이터와 제너레이터 (Iterators & Generators)

이터레이터와 제너레이터 (Iterators & Generators)

1. 이터러블과 이터레이터

개념 구분

용어 설명 예시
Iterable __iter__ 메서드가 있는 객체 list, str, dict, set
Iterator __iter____next__ 메서드가 있는 객체 iter(list), 파일 객체
┌──────────────────────────────────────────┐
│              Iterable                     │
│  ┌────────────────────────────────────┐  │
│  │    Iterator                         │  │
│  │    __iter__() → self               │  │
│  │    __next__() → 다음 값 or StopIteration │
│  └────────────────────────────────────┘  │
│  __iter__() → Iterator 반환              │
└──────────────────────────────────────────┘

for문의 동작 원리

# for item in iterable:
#     ...

# 위 코드는 아래와 동일
iterator = iter(iterable)  # __iter__() 호출
while True:
    try:
        item = next(iterator)  # __next__() 호출
        # 루프 본문 실행
    except StopIteration:
        break

예시

numbers = [1, 2, 3]

# iter()로 이터레이터 생성
iterator = iter(numbers)

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

2. 커스텀 이터레이터

iter__와 __next 구현

class Counter:
    """1부터 max까지 카운트하는 이터레이터"""

    def __init__(self, max_count):
        self.max_count = max_count
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.current += 1
        if self.current > self.max_count:
            raise StopIteration
        return self.current

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

이터러블과 이터레이터 분리

재사용 가능한 이터러블을 만들려면 분리가 필요합니다.

class Range:
    """재사용 가능한 range"""

    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __iter__(self):
        return RangeIterator(self.start, self.end)

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

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

# 여러 번 순회 가능
r = Range(1, 4)
print(list(r))  # [1, 2, 3]
print(list(r))  # [1, 2, 3] (다시 사용 가능)

3. 제너레이터 함수

yield 키워드를 사용하면 간단하게 이터레이터를 만들 수 있습니다.

기본 문법

def count_up_to(max_count):
    count = 1
    while count <= max_count:
        yield count  # 값을 반환하고 일시 정지
        count += 1

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

# 또는
gen = count_up_to(3)
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

동작 원리

count_up_to(3) 호출
    │
    ▼
┌─────────────────┐
│  count = 1      │
│  while True:    │
│    yield 1 ─────┼──▶ 반환, 일시 정지
│                 │
│  (next 호출)    │
│    count = 2    │
│    yield 2 ─────┼──▶ 반환, 일시 정지
│                 │
│  (next 호출)    │
│    count = 3    │
│    yield 3 ─────┼──▶ 반환, 일시 정지
│                 │
│  (next 호출)    │
│    while 종료   │
│  StopIteration  │
└─────────────────┘

여러 값을 yield

def multi_yield():
    yield "첫 번째"
    yield "두 번째"
    yield "세 번째"

for value in multi_yield():
    print(value)

4. 제너레이터 표현식

리스트 컴프리헨션과 비슷하지만 괄호를 사용합니다.

# 리스트 컴프리헨션 - 메모리에 전체 저장
squares_list = [x**2 for x in range(10)]

# 제너레이터 표현식 - 필요할 때 생성
squares_gen = (x**2 for x in range(10))

print(type(squares_list))  # <class 'list'>
print(type(squares_gen))   # <class 'generator'>

# 제너레이터는 한 번만 순회 가능
print(list(squares_gen))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_gen))  # [] (이미 소진됨)

메모리 효율성

import sys

# 리스트: 전체 메모리 사용
list_comp = [x for x in range(1000000)]
print(sys.getsizeof(list_comp))  # ~8MB

# 제너레이터: 최소 메모리
gen_exp = (x for x in range(1000000))
print(sys.getsizeof(gen_exp))    # ~200 bytes

5. yield from

다른 이터러블의 값을 위임합니다.

def chain(*iterables):
    for it in iterables:
        yield from it  # for item in it: yield item 과 동일

result = list(chain([1, 2], [3, 4], [5, 6]))
print(result)  # [1, 2, 3, 4, 5, 6]

재귀적 제너레이터

def flatten(nested):
    """중첩 리스트를 평탄화"""
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

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

6. 제너레이터 고급 기능

send() - 값 전달

제너레이터에 값을 보낼 수 있습니다.

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is not None:
            total += value

gen = accumulator()
print(next(gen))      # 0 (초기화)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

throw() - 예외 전달

def generator():
    try:
        yield 1
        yield 2
        yield 3
    except ValueError as e:
        yield f"예외 처리됨: {e}"

gen = generator()
print(next(gen))              # 1
print(gen.throw(ValueError, "테스트"))  # 예외 처리됨: 테스트

close() - 제너레이터 종료

def generator():
    try:
        yield 1
        yield 2
        yield 3
    finally:
        print("정리 작업")

gen = generator()
print(next(gen))  # 1
gen.close()       # "정리 작업" 출력

7. itertools 모듈

효율적인 이터레이터 도구를 제공합니다.

무한 이터레이터

from itertools import count, cycle, repeat

# count: 무한 카운터
for i in count(10, 2):  # 10부터 2씩 증가
    if i > 20:
        break
    print(i, end=" ")  # 10 12 14 16 18 20

# cycle: 무한 반복
colors = cycle(["빨강", "파랑", "초록"])
for _ in range(5):
    print(next(colors), end=" ")  # 빨강 파랑 초록 빨강 파랑

# repeat: 반복
for item in repeat("Hello", 3):
    print(item)  # Hello Hello Hello

조합 이터레이터

from itertools import chain, zip_longest, product, permutations, combinations

# chain: 여러 이터러블 연결
print(list(chain([1, 2], [3, 4])))  # [1, 2, 3, 4]

# zip_longest: 길이가 다른 이터러블 묶기
a = [1, 2, 3]
b = ["a", "b"]
print(list(zip_longest(a, b, fillvalue="-")))
# [(1, 'a'), (2, 'b'), (3, '-')]

# product: 데카르트 곱
print(list(product("AB", [1, 2])))
# [('A', 1), ('A', 2), ('B', 1), ('B', 2)]

# permutations: 순열
print(list(permutations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

# combinations: 조합
print(list(combinations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'C')]

필터링 이터레이터

from itertools import takewhile, dropwhile, filterfalse, compress

numbers = [1, 3, 5, 2, 4, 6]

# takewhile: 조건이 참인 동안
print(list(takewhile(lambda x: x < 5, numbers)))  # [1, 3]

# dropwhile: 조건이 참인 동안 건너뛰기
print(list(dropwhile(lambda x: x < 5, numbers)))  # [5, 2, 4, 6]

# filterfalse: 조건이 거짓인 것만
print(list(filterfalse(lambda x: x % 2, numbers)))  # [2, 4, 6]

# compress: 선택자로 필터링
data = ["A", "B", "C", "D"]
selectors = [1, 0, 1, 0]
print(list(compress(data, selectors)))  # ['A', 'C']

그룹화

from itertools import groupby

data = [
    {"name": "Alice", "dept": "HR"},
    {"name": "Bob", "dept": "IT"},
    {"name": "Charlie", "dept": "HR"},
    {"name": "David", "dept": "IT"},
]

# 정렬 필수!
data.sort(key=lambda x: x["dept"])

for dept, group in groupby(data, key=lambda x: x["dept"]):
    print(f"{dept}: {[p['name'] for p in group]}")
# HR: ['Alice', 'Charlie']
# IT: ['Bob', 'David']

슬라이싱

from itertools import islice

# 무한 이터레이터에서 일부만 추출
from itertools import count

first_10 = list(islice(count(1), 10))
print(first_10)  # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 시작, 끝, 스텝 지정
result = list(islice(range(100), 10, 20, 2))
print(result)  # [10, 12, 14, 16, 18]

8. 지연 평가 (Lazy Evaluation)

제너레이터는 값을 미리 계산하지 않고 필요할 때 생성합니다.

대용량 파일 처리

def read_large_file(filepath):
    """한 줄씩 읽는 제너레이터"""
    with open(filepath, "r") as f:
        for line in f:
            yield line.strip()

# 메모리 효율적으로 처리
for line in read_large_file("huge_file.txt"):
    if "ERROR" in line:
        print(line)

파이프라인 처리

def numbers():
    for i in range(1, 1000001):
        yield i

def even_only(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n ** 2

def less_than(nums, limit):
    for n in nums:
        if n >= limit:
            break
        yield n

# 파이프라인: 메모리 효율적
pipeline = less_than(squared(even_only(numbers())), 100)
print(list(pipeline))  # [4, 16, 36, 64]

9. 무한 시퀀스

피보나치 수열

def fibonacci():
    """무한 피보나치 수열"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 처음 10개
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

소수 생성기

def primes():
    """무한 소수 생성"""
    yield 2
    candidate = 3
    found = [2]
    while True:
        if all(candidate % p != 0 for p in found):
            found.append(candidate)
            yield candidate
        candidate += 2

# 처음 10개 소수
from itertools import islice
print(list(islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

10. 요약

개념 설명
Iterable __iter__ 구현, for문에 사용 가능
Iterator __iter__ + __next__ 구현
Generator yield를 사용하는 함수
Generator Expression (x for x in iterable)
yield from 다른 이터러블 위임
send() 제너레이터에 값 전달
Lazy Evaluation 필요할 때 값 생성

11. 연습 문제

연습 1: 청크 분할

리스트를 지정된 크기의 청크로 나누는 제너레이터를 작성하세요.

# chunk([1,2,3,4,5], 2) → [1,2], [3,4], [5]

연습 2: 윈도우 슬라이딩

슬라이딩 윈도우를 생성하는 제너레이터를 작성하세요.

# sliding_window([1,2,3,4,5], 3) → (1,2,3), (2,3,4), (3,4,5)

연습 3: 트리 순회

이진 트리를 순회하는 제너레이터를 작성하세요.


다음 단계

05_Closures_and_Scope.md에서 변수 스코프와 클로저를 배워봅시다!

to navigate between lessons