포스트

[Python 100일 챌린지] Day 64 - 이터레이터와 제너레이터 심화

[Python 100일 챌린지] Day 64 - 이터레이터와 제너레이터 심화

이터레이터의 비밀을 파헤쳐봐요! 🔍✨

for 루프가 어떻게 동작하는지 궁금하지 않으셨나요? 파이썬의 for, map(), filter() 모두 내부적으로 이터레이터를 사용해요! 오늘은 그 동작 원리를 깊이 파고들어, 나만의 이터레이터를 만들고 itertools로 강력한 데이터 처리 파이프라인을 구축할 거예요. pandas, Django QuerySet도 모두 이터레이터 패턴을 활용하고 있답니다! 😊

어제 배운 제너레이터를 한 단계 더 업그레이드하는 시간이에요!

(35분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 이터레이터 프로토콜 이해하기

한 줄 설명

이터레이터 프로토콜 = for 루프의 비밀 레시피 📜🔐

for 루프가 어떻게 동작하는지 이해하면, 어떤 객체든 for 루프에서 사용할 수 있게 만들 수 있어요!

1.1 이터레이터란?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 리스트는 이터러블(iterable)
numbers = [1, 2, 3, 4, 5]

# iter()로 이터레이터 얻기
iterator = iter(numbers)

# next()로 값 하나씩 가져오기
print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3

# for 루프는 내부적으로 iter()와 next() 사용
for num in numbers:
    print(num)

💡 실생활 비유: 책을 읽을 때, 책(이터러블)을 열면 책갈피(이터레이터)가 현재 페이지를 기억하고, 다음 페이지로 넘기는(next) 것과 같아요!

1.2 이터레이터 프로토콜

이터레이터는 두 개의 메서드를 구현해야 해요:

  • __iter__(): 자기 자신을 반환해요
  • __next__(): 다음 값을 반환해요 (값이 없으면 StopIteration 발생)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 리스트가 이터레이터를 만드는 과정
numbers = [1, 2, 3]

# 1단계: iter() 호출
iterator = iter(numbers)  # numbers.__iter__()

# 2단계: next() 반복 호출
try:
    while True:
        item = next(iterator)  # iterator.__next__()
        print(item)
except StopIteration:
    pass  # 순회 완료

이렇게 동작해요! 🎬

  1. for 문이 시작되면 iter() 호출해요
  2. 매번 next()로 값을 가져와요
  3. StopIteration 예외가 나오면 루프를 종료해요

1.3 이터러블 vs 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 이터러블(Iterable): __iter__()만 구현
class MyIterable:
    def __iter__(self):
        return iter([1, 2, 3])

# 이터레이터(Iterator): __iter__()와 __next__() 모두 구현
class MyIterator:
    def __init__(self):
        self.data = [1, 2, 3]
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.data):
            raise StopIteration
        value = self.data[self.index]
        self.index += 1
        return value

# 사용
for item in MyIterable():
    print(item)

for item in MyIterator():
    print(item)

차이점 요약! 📊

  • 이터러블: 반복 가능한 객체 (리스트, 튜플, 문자열 등)
  • 이터레이터: 실제로 값을 하나씩 꺼내주는 객체

🎯 학습 목표 2: 커스텀 이터레이터 만들기

한 줄 설명

커스텀 이터레이터 = 나만의 for 루프 규칙 🎨📋

원하는 방식으로 데이터를 순회할 수 있는 나만의 이터레이터를 만들어요!

2.1 기본 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Countdown:
    """카운트다운 이터레이터"""
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        self.current -= 1
        return self.current + 1

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

💡 핵심: __iter__()는 자기 자신을 반환하고, __next__()는 다음 값을 반환해요!

2.2 실전: 파일 라인 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FileLineIterator:
    """파일을 한 줄씩 읽는 이터레이터"""
    def __init__(self, filename):
        self.filename = filename
        self.file = None

    def __iter__(self):
        self.file = open(self.filename, 'r', encoding='utf-8')
        return self

    def __next__(self):
        line = self.file.readline()

        if not line:
            self.file.close()
            raise StopIteration

        return line.strip()

# 사용
# for line in FileLineIterator('data.txt'):
#     print(line)

실무에서 이렇게 써요! 💼

  • 대용량 파일을 메모리에 부담 없이 한 줄씩 처리해요
  • 로그 파일 분석, CSV 처리 등에 활용해요

2.3 실전: 범위 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Range:
    """range()를 흉내낸 이터레이터"""
    def __init__(self, start, end, step=1):
        self.start = start
        self.end = end
        self.step = step
        self.current = start

    def __iter__(self):
        self.current = self.start
        return self

    def __next__(self):
        if (self.step > 0 and self.current >= self.end) or \
           (self.step < 0 and self.current <= self.end):
            raise StopIteration

        value = self.current
        self.current += self.step
        return value

# 사용
for num in Range(0, 10, 2):
    print(num, end=" ")
# 출력: 0 2 4 6 8

💡 실생활 비유: 엘리베이터가 특정 층만 멈추는 것처럼, 원하는 간격으로 숫자를 생성할 수 있어요!


🎯 학습 목표 3: 제너레이터 고급 기법

한 줄 설명

제너레이터 고급 기법 = 이터레이터의 슈퍼파워 🦸‍♂️⚡

yield from, send() 같은 강력한 기능으로 제너레이터를 한 단계 업그레이드해요!

3.1 yield from

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def generator1():
    yield 1
    yield 2

def generator2():
    yield 3
    yield 4

# 여러 제너레이터 결합 (기본 방법)
def combined_basic():
    for value in generator1():
        yield value
    for value in generator2():
        yield value

# yield from 사용 (권장) ⭐
def combined_advanced():
    yield from generator1()
    yield from generator2()

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

yield from을 쓸까요? 🤔

  • 코드가 훨씬 간결해요!
  • 제너레이터를 쉽게 조합할 수 있어요
  • 중첩 데이터 구조를 평탄화할 때 유용해요

3.2 양방향 제너레이터 (send)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def echo_generator():
    """값을 받아서 처리하는 제너레이터"""
    value = None
    while True:
        received = yield value
        if received is None:
            break
        value = f"받은 값: {received}"

# 사용
gen = echo_generator()
next(gen)  # 제너레이터 시작

print(gen.send("Hello"))     # 받은 값: Hello
print(gen.send("World"))     # 받은 값: World
print(gen.send(42))          # 받은 값: 42

💡 실생활 비유: 대화하는 것처럼, 제너레이터에 값을 보내면(send) 제너레이터가 응답(yield)해줘요!

3.3 제너레이터 체이닝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def numbers(n):
    """숫자 생성"""
    for i in range(n):
        yield i

def square(gen):
    """제곱"""
    for num in gen:
        yield num ** 2

def add_one(gen):
    """1 더하기"""
    for num in gen:
        yield num + 1

# 체이닝 🔗
pipeline = add_one(square(numbers(5)))
print(list(pipeline))
# [1, 2, 5, 10, 17]

파이프라인의 장점! 🚰

  • 각 단계를 독립적으로 테스트할 수 있어요
  • 재사용 가능해요
  • 메모리 효율적이에요

3.4 실전: 데이터 스트림 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def read_logs():
    """로그 파일 읽기 (시뮬레이션)"""
    logs = [
        "2025-01-01 10:00:00 INFO User logged in",
        "2025-01-01 10:05:00 ERROR Database connection failed",
        "2025-01-01 10:10:00 INFO Data saved",
        "2025-01-01 10:15:00 WARNING High memory usage",
        "2025-01-01 10:20:00 ERROR Timeout occurred"
    ]
    for log in logs:
        yield log

def filter_errors(logs):
    """에러만 필터링"""
    for log in logs:
        if 'ERROR' in log:
            yield log

def extract_message(logs):
    """메시지 추출"""
    for log in logs:
        parts = log.split(' ', 3)
        if len(parts) >= 4:
            yield parts[3]

# 파이프라인 실행
error_messages = extract_message(filter_errors(read_logs()))

for message in error_messages:
    print(f"{message}")

# 출력:
# ❌ Database connection failed
# ❌ Timeout occurred

실무에서 이렇게 써요! 🎯

  • 로그 분석, ETL 파이프라인
  • 데이터 전처리 및 변환
  • 실시간 스트림 처리

🎯 학습 목표 4: itertools 모듈 활용하기

한 줄 설명

itertools = 이터레이터의 스위스 아미 나이프 🔧🎩

파이썬이 제공하는 강력한 이터레이터 도구 모음이에요!

4.1 무한 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import itertools

# count: 무한 카운터 ♾️
for i in itertools.count(10, 2):
    print(i, end=" ")
    if i >= 20:
        break
# 출력: 10 12 14 16 18 20

# cycle: 무한 반복 🔄
counter = 0
for item in itertools.cycle(['A', 'B', 'C']):
    print(item, end=" ")
    counter += 1
    if counter >= 7:
        break
# 출력: A B C A B C A

# repeat: N번 반복
print(list(itertools.repeat('Hello', 3)))
# ['Hello', 'Hello', 'Hello']

무한 이터레이터의 활용!

  • 카운터, ID 생성기
  • 순환 작업 (로드 밸런싱)
  • 기본값 생성

4.2 조합 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import itertools

# chain: 여러 이터러블 연결 🔗
list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list(itertools.chain(list1, list2)))
# [1, 2, 3, 4, 5, 6]

# zip_longest: 가장 긴 것에 맞춰 zip
list1 = [1, 2, 3]
list2 = ['A', 'B']
print(list(itertools.zip_longest(list1, list2, fillvalue='X')))
# [(1, 'A'), (2, 'B'), (3, 'X')]

# product: 카르테시안 곱 (모든 조합)
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
print(list(itertools.product(colors, sizes)))
# [('red', 'S'), ('red', 'M'), ('red', 'L'),
#  ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]

# permutations: 순열 (순서 O)
print(list(itertools.permutations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

# combinations: 조합 (순서 X)
print(list(itertools.combinations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 3)]

💡 실생활 비유: 옷을 고를 때, product는 “색상 × 사이즈” 모든 조합, combinations는 “3벌 중 2벌 고르기”예요!

4.3 필터링 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import itertools

# takewhile: 조건이 True인 동안만 🎯
numbers = [1, 2, 3, 4, 5, 1, 2]
print(list(itertools.takewhile(lambda x: x < 4, numbers)))
# [1, 2, 3]

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

# filterfalse: filter의 반대
print(list(itertools.filterfalse(lambda x: x % 2 == 0, range(10))))
# [1, 3, 5, 7, 9]

# islice: 슬라이싱 ✂️
print(list(itertools.islice(range(10), 2, 8, 2)))
# [2, 4, 6]

4.4 그룹화 이터레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import itertools

# groupby: 연속된 같은 값 그룹화
data = [1, 1, 2, 2, 2, 3, 3, 1, 1]
for key, group in itertools.groupby(data):
    print(f"{key}: {list(group)}")
# 출력:
# 1: [1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]
# 1: [1, 1]

# 실전: 문자열 압축
def compress(s):
    """연속된 문자 압축"""
    result = []
    for char, group in itertools.groupby(s):
        count = len(list(group))
        result.append(f"{char}{count if count > 1 else ''}")
    return ''.join(result)

print(compress("aaabbccccd"))  # a3b2c4d

실무에서 이렇게 써요! 💼

  • 데이터 압축 알고리즘
  • 연속된 이벤트 그룹화
  • 시계열 데이터 집계

4.5 실전 예제: 배치 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import itertools

def chunked(iterable, n):
    """이터러블을 n개씩 묶음"""
    iterator = iter(iterable)
    while True:
        chunk = list(itertools.islice(iterator, n))
        if not chunk:
            break
        yield chunk

# 사용
data = range(1, 11)
for batch in chunked(data, 3):
    print(batch)
# 출력:
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10]

실무 활용! 🎯

  • 대량 DB insert (배치 처리)
  • 머신러닝 미니배치
  • API 요청 분할

4.6 실전 예제: 윈도우 슬라이딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections import deque
import itertools

def sliding_window(iterable, n):
    """슬라이딩 윈도우"""
    iterator = iter(iterable)
    window = deque(itertools.islice(iterator, n), maxlen=n)

    if len(window) == n:
        yield tuple(window)

    for item in iterator:
        window.append(item)
        yield tuple(window)

# 사용
for window in sliding_window([1, 2, 3, 4, 5], 3):
    print(window)
# 출력:
# (1, 2, 3)
# (2, 3, 4)
# (3, 4, 5)

실무 활용! 💡

  • 이동 평균 계산
  • 시계열 분석
  • 패턴 인식

💡 오늘의 핵심 요약

  1. 이터레이터 프로토콜:
    • __iter__(): 자기 자신 반환해요
    • __next__(): 다음 값 반환해요
    • StopIteration: 순회 종료 신호예요
  2. 제너레이터 고급 기법:
    • yield from: 다른 제너레이터 위임해요
    • send(): 양방향 통신 가능해요
    • 체이닝: 파이프라인 구성해요
  3. itertools 모듈:
    • 무한 이터레이터: count, cycle, repeat
    • 조합: chain, product, permutations, combinations
    • 필터링: takewhile, dropwhile, filterfalse
    • 그룹화: groupby, islice
  4. 활용 사례:
    • 🗂️ 대용량 데이터 처리
    • 🚰 데이터 파이프라인
    • 📊 배치 처리
    • 🔄 순환 작업

🧪 연습 문제

문제 1: 윈도우 슬라이딩 이터레이터

연속된 N개 요소의 윈도우를 생성하는 이터레이터를 작성하세요.

1
2
3
4
5
6
7
8
9
10
11
def sliding_window(iterable, n):
    # 여기에 코드 작성
    pass

# 사용
for window in sliding_window([1, 2, 3, 4, 5], 3):
    print(window)
# 출력:
# (1, 2, 3)
# (2, 3, 4)
# (3, 4, 5)
💡 힌트

단계별 힌트:

  1. collections.deque를 사용하여 고정 크기 윈도우를 만들어요
  2. itertools.islice로 첫 N개 항목을 채워요
  3. 나머지 항목들을 하나씩 추가하면서 yield해요

핵심 키워드: deque(maxlen=n), itertools.islice, yield tuple(window)

정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import itertools
from collections import deque

def sliding_window(iterable, n):
    """윈도우 슬라이딩 이터레이터"""
    iterator = iter(iterable)
    window = deque(itertools.islice(iterator, n), maxlen=n)

    if len(window) == n:
        yield tuple(window)

    for item in iterator:
        window.append(item)
        yield tuple(window)

# 테스트
for window in sliding_window([1, 2, 3, 4, 5], 3):
    print(window)

# 문자열에도 사용 가능
for window in sliding_window("ABCDE", 3):
    print(''.join(window))
# 출력: ABC, BCD, CDE

어떻게 동작할까요?

  1. deque(maxlen=n): 최대 크기가 n인 큐를 만들어요
  2. 새 항목이 추가되면 오래된 항목이 자동으로 제거돼요
  3. 매번 현재 윈도우를 튜플로 반환해요

문제 2: 피보나치 이터레이터

무한 피보나치 수열을 생성하는 이터레이터를 클래스로 작성하세요.

💡 힌트

단계별 힌트:

  1. __init__에서 초기값 a=0, b=1을 설정해요
  2. __iter__는 self를 반환해요
  3. __next__에서 다음 피보나치 수를 계산하고 반환해요

핵심 키워드: __iter__, __next__, a, b = b, a + b

정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Fibonacci:
    """무한 피보나치 수열 이터레이터"""
    def __init__(self):
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        result = self.a
        self.a, self.b = self.b, self.a + self.b
        return result

# 테스트
fib = Fibonacci()
for i, num in enumerate(fib):
    print(num, end=" ")
    if i >= 9:
        break
# 출력: 0 1 1 2 3 5 8 13 21 34

왜 클래스를 쓸까요?

  • 상태(a, b)를 인스턴스 변수로 저장해요
  • 여러 개의 독립적인 피보나치 이터레이터를 만들 수 있어요

📝 오늘 배운 내용 정리

  1. 이터레이터 프로토콜: __iter____next__로 for 루프의 동작 원리를 이해했어요
  2. 커스텀 이터레이터: 나만의 순회 규칙을 가진 이터레이터를 만들 수 있어요
  3. 제너레이터 고급 기법: yield from, send()로 더 강력한 제너레이터를 만들어요
  4. itertools 모듈: 실무에서 자주 쓰는 이터레이터 도구들을 마스터했어요

🔗 관련 자료


📚 이전 학습

Day 63: 제너레이터 기초 ⭐⭐⭐

어제는 yield 키워드로 메모리 효율적인 제너레이터를 만드는 방법을 배웠어요!

📚 다음 학습

Day 65: 컨텍스트 매니저 ⭐⭐⭐

내일은 with 문의 동작 원리와 리소스를 안전하게 관리하는 컨텍스트 매니저를 배워요!


“복잡한 것도 원리를 알면 간단해져요!” 🚀

Day 64/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.