포스트

[Python 100일 챌린지] Day 63 - 제너레이터 기초

[Python 100일 챌린지] Day 63 - 제너레이터 기초

제너레이터는 메모리 마법사예요! 🧙‍♂️✨

1억 개짜리 리스트를 만들면 컴퓨터 메모리가 뻗어버리죠? 제너레이터를 쓰면 메모리는 딱 1개 값만 쓰면서도 1억 개를 다룰 수 있어요! Netflix가 영화 목록 전체를 보내지 않고 스크롤할 때마다 10개씩 보여주는 것처럼, 제너레이터는 필요할 때만 값을 만들어줘요. 😊

대용량 데이터, 무한 시퀀스, 실시간 스트리밍… 실무에서 필수예요!

(30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식

  • Phase 2: 리스트와 컴프리헨션
  • Phase 3: 함수와 반복문

🎯 학습 목표 1: 제너레이터의 개념과 필요성 이해하기

한 줄 설명

제너레이터 = 필요할 때만 값을 만드는 게으른 공장 🏭💤

재고를 미리 쌓아두지 않고, 주문이 들어올 때마다 딱 필요한 만큼만 생산하는 공장 같아요!

1.1 문제 상황: 메모리 낭비

1
2
3
4
5
6
7
8
9
10
11
# 일반 함수: 모든 데이터를 메모리에 저장
def get_numbers(n):
    """1부터 n까지의 숫자를 리스트로 반환"""
    result = []
    for i in range(1, n + 1):
        result.append(i)
    return result

# 문제: 큰 숫자를 다룰 때 메모리 부족
numbers = get_numbers(10_000_000)  # 리스트 전체가 메모리에!
print(numbers[0])  # 첫 번째 값만 필요한데...

무엇이 문제일까요? 🤔

  • 1천만 개의 숫자를 전부 메모리에 저장해요
  • 첫 번째 값만 필요한데 전부 만들어야 해요
  • 메모리 낭비! 💸

💡 실생활 비유: 손님이 커피 1잔만 시켰는데 카페에서 커피 1000잔을 미리 만들어놓는 격이에요. 낭비죠!

1.2 제너레이터의 해결책

1
2
3
4
5
6
7
8
9
def get_numbers_generator(n):
    """1부터 n까지의 숫자를 하나씩 생성"""
    for i in range(1, n + 1):
        yield i  # yield: 값을 하나씩 반환

# 메모리 효율적!
numbers_gen = get_numbers_generator(10_000_000)
print(next(numbers_gen))  # 1 (필요할 때만 생성)
print(next(numbers_gen))  # 2

제너레이터의 장점 ✨:

  • 게으른 평가(Lazy Evaluation): 필요할 때만 값 생성해요
  • 메모리 효율적: 한 번에 하나의 값만 메모리에 유지해요
  • 무한 시퀀스 가능: 메모리 제한 없이 무한히 생성 가능해요

1.3 메모리 비교 - 충격적인 차이!

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys

# 리스트 컴프리헨션
list_comp = [x ** 2 for x in range(10000)]
print(f"리스트 크기: {sys.getsizeof(list_comp):,} bytes")

# 제너레이터 표현식
gen_exp = (x ** 2 for x in range(10000))
print(f"제너레이터 크기: {sys.getsizeof(gen_exp):,} bytes")

# 출력:
# 리스트 크기: 87,624 bytes
# 제너레이터 크기: 112 bytes (약 780배 차이!)

780배 차이! 🚀 같은 일을 하는데 메모리는 1/780만 써요!

💡 실생활 비유: 영화 전체를 다운로드(리스트)하지 않고 스트리밍(제너레이터)으로 보는 것과 비슷해요!


🎯 학습 목표 2: yield 키워드로 제너레이터 만들기

한 줄 설명

yield = “잠깐 멈춰! 값 하나 드릴게요” 🛑🎁

return은 함수를 완전히 종료하지만, yield는 값을 주고 일시정지 했다가, 다시 이어서 실행해요!

2.1 기본 제너레이터

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
35
def simple_generator():
    """가장 간단한 제너레이터"""
    print("첫 번째 yield 전")
    yield 1

    print("두 번째 yield 전")
    yield 2

    print("세 번째 yield 전")
    yield 3

    print("제너레이터 종료")

# 사용
gen = simple_generator()

print(next(gen))
# 출력:
# 첫 번째 yield 전
# 1

print(next(gen))
# 출력:
# 두 번째 yield 전
# 2

print(next(gen))
# 출력:
# 세 번째 yield 전
# 3

print(next(gen))
# 출력:
# 제너레이터 종료
# StopIteration 예외 발생

어떻게 동작할까요? 🤔

  1. next() 호출하면 yield까지 실행해요
  2. yield에서 값을 주고 일시정지해요
  3. 다음 next() 호출하면 멈췄던 곳부터 재개해요!

2.2 for 루프와 함께 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def countdown(n):
    """카운트다운 제너레이터"""
    print(f"카운트다운 시작: {n}")
    while n > 0:
        yield n
        n -= 1
    print("발사! 🚀")

# for 루프는 자동으로 StopIteration 처리해요
for count in countdown(5):
    print(count)

# 출력:
# 카운트다운 시작: 5
# 5
# 4
# 3
# 2
# 1
# 발사! 🚀

for 루프의 장점! 💡

  • next()를 수동으로 안 불러도 돼요
  • StopIteration 예외를 자동으로 처리해요

2.3 yield의 동작 원리 - 이해하기

1
2
3
4
5
6
7
8
9
10
11
12
def my_range(start, end):
    """range()를 흉내낸 제너레이터"""
    current = start
    while current < end:
        yield current  # 여기서 멈추고 값 반환
        # next() 호출 시 여기서부터 재개
        current += 1

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

💡 실생활 비유: 책을 읽다가 책갈피를 끼워두고(yield), 나중에 그 페이지부터 다시 읽는 것(next)과 비슷해요!

2.4 제너레이터 함수 vs 일반 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 일반 함수
def normal_function():
    return [1, 2, 3]

# 제너레이터 함수
def generator_function():
    yield 1
    yield 2
    yield 3

# 차이점
print(normal_function())        # [1, 2, 3] (리스트)
print(generator_function())     # <generator object> (제너레이터)

# 제너레이터를 리스트로 변환
print(list(generator_function()))  # [1, 2, 3]

핵심 차이!

  • 일반 함수: return으로 전체 결과를 한 번에 반환해요
  • 제너레이터: yield로 값을 하나씩 반환해요

🎯 학습 목표 3: 제너레이터 표현식 활용하기

한 줄 설명

제너레이터 표현식 = 리스트 컴프리헨션의 게으른 버전 💤

[ ] 대신 ( )만 쓰면 돼요!

3.1 제너레이터 표현식 기본

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 리스트 컴프리헨션 (한 번에 모든 값 생성)
list_comp = [x ** 2 for x in range(10)]
print(type(list_comp))  # <class 'list'>
print(list_comp)        # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 제너레이터 표현식 (필요할 때만 값 생성)
gen_exp = (x ** 2 for x in range(10))
print(type(gen_exp))    # <class 'generator'>
print(gen_exp)          # <generator object>

# 값 하나씩 가져오기
print(next(gen_exp))    # 0
print(next(gen_exp))    # 1

# 나머지 값들 가져오기
print(list(gen_exp))    # [4, 9, 16, 25, 36, 49, 64, 81]

차이점 요약! 📊

  • [ ]: 리스트 - 즉시 모든 값 생성
  • ( ): 제너레이터 - 필요할 때만 생성

3.2 실전: 대용량 파일 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 나쁜 예: 전체 파일을 메모리에 로드 ❌
def read_file_bad(filename):
    """메모리에 모든 줄을 저장"""
    with open(filename, 'r') as f:
        return f.readlines()  # 전체 파일이 메모리에!

# 좋은 예: 제너레이터로 한 줄씩 읽기 ✅
def read_file_good(filename):
    """한 줄씩 생성"""
    with open(filename, 'r') as f:
        for line in f:
            yield line.strip()

# 사용 (가정: large_file.txt가 1GB 크기)
# for line in read_file_good('large_file.txt'):
#     if 'ERROR' in line:
#         print(line)
#         break  # 필요한 줄을 찾으면 중단 (메모리 절약!)

실무에서 이렇게 써요! 🎯

  • 로그 파일 분석할 때
  • CSV 파일 처리할 때
  • 대용량 데이터 전처리할 때

3.3 제너레이터 표현식 응용

1
2
3
4
5
6
7
8
9
10
11
12
13
# 짝수의 제곱만 필터링
even_squares = (x ** 2 for x in range(20) if x % 2 == 0)
print(list(even_squares))
# [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

# 문자열 처리
text = "Hello World"
lowercase = (char.lower() for char in text if char.isalpha())
print(''.join(lowercase))  # helloworld

# sum, max, min 등과 함께 사용
sum_of_squares = sum(x ** 2 for x in range(100))
print(sum_of_squares)  # 328350

팁! 💡 sum(), max(), min() 같은 함수에 제너레이터 표현식을 쓰면 메모리를 아껴요!


🎯 학습 목표 4: 실전 제너레이터 예제

예제 1: 무한 시퀀스 - 영원히!

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
def infinite_sequence():
    """무한히 숫자를 생성하는 제너레이터"""
    num = 0
    while True:
        yield num
        num += 1

# 사용
gen = infinite_sequence()

# 필요한 만큼만 가져오기
for i in gen:
    print(i, end=" ")
    if i >= 9:
        break
# 출력: 0 1 2 3 4 5 6 7 8 9

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

# 사용
fib = fibonacci()
for _ in range(10):
    print(next(fib), end=" ")
# 출력: 0 1 1 2 3 5 8 13 21 34

무한 시퀀스의 장점! ♾️

  • 리스트로는 불가능해요 (메모리 부족)
  • 제너레이터는 필요한 만큼만 생성해요

예제 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
def read_data():
    """데이터 읽기"""
    data = ['  Alice  ', '  BOB  ', '  charlie  ', '  DAVID  ']
    for item in data:
        yield item

def clean_data(data_gen):
    """데이터 정제"""
    for item in data_gen:
        yield item.strip()

def normalize_data(data_gen):
    """데이터 정규화"""
    for item in data_gen:
        yield item.capitalize()

# 파이프라인 구성 🔧
pipeline = normalize_data(clean_data(read_data()))

# 실행
for name in pipeline:
    print(name)

# 출력:
# Alice
# Bob
# Charlie
# David

파이프라인의 장점! 🚰

  • 각 단계를 깔끔하게 분리해요
  • 메모리 효율적이에요 (한 번에 1개씩 처리)
  • 재사용 가능해요

예제 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
def batch_data(iterable, batch_size):
    """데이터를 배치로 나누는 제너레이터"""
    batch = []
    for item in iterable:
        batch.append(item)
        if len(batch) == batch_size:
            yield batch
            batch = []

    # 남은 데이터 처리
    if batch:
        yield batch

# 사용
data = range(1, 21)  # 1부터 20까지

for batch in batch_data(data, batch_size=5):
    print(batch)

# 출력:
# [1, 2, 3, 4, 5]
# [6, 7, 8, 9, 10]
# [11, 12, 13, 14, 15]
# [16, 17, 18, 19, 20]

실무에서 이렇게 써요! 🎯

  • 머신러닝 학습 시 데이터를 배치로 나눌 때
  • DB에 대량 insert할 때 (한 번에 1000개씩)

예제 4: CSV 파일 스트리밍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def csv_reader(filename):
    """CSV 파일을 한 줄씩 읽는 제너레이터"""
    with open(filename, 'r', encoding='utf-8') as file:
        # 헤더 건너뛰기
        next(file)

        for line in file:
            # 각 줄을 딕셔너리로 변환
            fields = line.strip().split(',')
            yield {
                'name': fields[0],
                'age': int(fields[1]),
                'city': fields[2]
            }

# 사용 예시
# for row in csv_reader('users.csv'):
#     if row['age'] >= 18:
#         print(f"{row['name']}님은 성인입니다")

실무 활용! 💼

  • 수백만 줄짜리 CSV도 메모리 부담 없이 처리해요
  • 조건에 맞는 데이터만 찾으면 바로 중단할 수 있어요

예제 5: 실시간 로그 모니터링

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

def follow_log(filename):
    """실시간으로 로그 파일을 모니터링하는 제너레이터"""
    with open(filename, 'r') as file:
        # 파일 끝으로 이동
        file.seek(0, 2)

        while True:
            line = file.readline()

            if not line:
                # 새 줄이 없으면 잠시 대기
                time.sleep(0.1)
                continue

            yield line.strip()

# 사용 예시
# for line in follow_log('app.log'):
#     if 'ERROR' in line:
#         print(f"⚠️  오류 발생: {line}")

실무에서 이렇게 써요! 🔍

  • 서버 로그 실시간 모니터링
  • 에러 발생 즉시 알림 보내기

예제 6: 숫자 범위 제너레이터 - 실수도 OK!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def number_range(start, end, step=1):
    """range()의 강화 버전 (실수 지원)"""
    current = start
    if step > 0:
        while current < end:
            yield current
            current += step
    else:
        while current > end:
            yield current
            current += step

# 사용
print("정수:")
print(list(number_range(0, 10, 2)))
# [0, 2, 4, 6, 8]

print("\n실수:")
print(list(number_range(0, 1, 0.1)))
# [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]

print("\n역순:")
print(list(number_range(10, 0, -2)))
# [10, 8, 6, 4, 2]

range()의 한계 극복! 🚀

  • Python의 range()는 정수만 되는데, 이건 실수도 돼요!

💡 오늘의 핵심 요약

1. 제너레이터

  • yield 키워드로 값을 하나씩 생성해요
  • 메모리 효율적이에요 (게으른 평가)
  • 무한 시퀀스도 가능해요!

2. 제너레이터 함수

1
2
3
4
def my_gen():
    yield 1
    yield 2
    yield 3

3. 제너레이터 표현식

1
gen = (x ** 2 for x in range(10))  # ( ) 사용!

4. 사용 방법

  • next(gen): 다음 값 가져오기
  • for item in gen: 모든 값 순회
  • list(gen): 리스트로 변환

5. 활용 사례

  • 🗂️ 대용량 파일 처리
  • ♾️ 무한 시퀀스
  • 🚰 데이터 파이프라인
  • 📡 실시간 스트리밍

🧪 연습 문제

문제 1: 소수 제너레이터

N 이하의 모든 소수를 생성하는 제너레이터를 작성하세요.

1
2
3
4
5
6
7
8
def primes(n):
    # 여기에 코드 작성
    pass

# 사용
for p in primes(20):
    print(p, end=" ")
# 출력: 2 3 5 7 11 13 17 19
💡 힌트

단계별 힌트:

  1. 2부터 n까지 반복해요
  2. 각 숫자가 소수인지 체크하는 함수(is_prime)를 만들어요
  3. 소수면 yield로 반환해요

핵심 키워드: is_prime(), yield, range(2, int(num ** 0.5) + 1)

소수 체크 방법: 2부터 √num까지 나눠서 나누어떨어지는 게 없으면 소수예요!

정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def primes(n):
    """N 이하의 소수를 생성하는 제너레이터"""
    def is_prime(num):
        if num < 2:
            return False
        for i in range(2, int(num ** 0.5) + 1):
            if num % i == 0:
                return False
        return True

    for num in range(2, n + 1):
        if is_prime(num):
            yield num

# 테스트
print(list(primes(20)))
# [2, 3, 5, 7, 11, 13, 17, 19]

# 큰 범위도 메모리 효율적으로 처리
count = 0
for p in primes(1_000_000):
    count += 1
print(f"100만 이하의 소수 개수: {count}")

왜 제너레이터가 좋을까요?

  • 100만 개의 소수를 리스트로 만들면 메모리를 많이 써요
  • 제너레이터는 필요할 때만 생성하니까 메모리를 아껴요!

문제 2: 파일 필터링 제너레이터

텍스트 파일에서 특정 키워드를 포함한 줄만 반환하는 제너레이터를 작성하세요.

💡 힌트

단계별 힌트:

  1. with open()으로 파일을 열어요
  2. enumerate(file, 1)로 줄 번호와 내용을 함께 가져와요
  3. 키워드가 포함된 줄만 yield로 반환해요
  4. 대소문자 구분 없이 검색하려면 .lower() 사용해요

핵심 키워드: enumerate(), in, yield, lower()

정답 코드
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
def filter_lines(filename, keyword):
    """파일에서 키워드를 포함한 줄만 반환"""
    with open(filename, 'r', encoding='utf-8') as file:
        for line_num, line in enumerate(file, 1):
            if keyword.lower() in line.lower():
                yield (line_num, line.strip())

# 테스트 (가상의 파일)
# for line_num, line in filter_lines('log.txt', 'error'):
#     print(f"라인 {line_num}: {line}")

# 실제 테스트를 위한 파일 생성 예시
with open('test.txt', 'w') as f:
    f.write("This is a test\n")
    f.write("Error occurred here\n")
    f.write("Normal line\n")
    f.write("Another ERROR message\n")

# 사용
for line_num, line in filter_lines('test.txt', 'error'):
    print(f"라인 {line_num}: {line}")

# 출력:
# 라인 2: Error occurred here
# 라인 4: Another ERROR message

왜 제너레이터가 좋을까요?

  • 수 GB짜리 로그 파일도 메모리 부담 없이 처리해요
  • 원하는 줄을 찾으면 바로 중단할 수 있어요!

📝 오늘 배운 내용 정리

  1. 제너레이터: yield로 값을 하나씩 생성하는 메모리 효율적인 방법이에요
  2. 게으른 평가: 필요할 때만 값을 만들어요
  3. 제너레이터 표현식: (x for x in range(10)) - 리스트 컴프리헨션의 게으른 버전이에요
  4. 실무 활용: 대용량 파일, 무한 시퀀스, 데이터 파이프라인, 실시간 스트리밍

🔗 관련 자료


📚 이전 학습

Day 62: 데코레이터 심화 ⭐⭐⭐

어제는 매개변수가 있는 데코레이터, 클래스 데코레이터, 데코레이터 체이닝을 배웠어요!

📚 다음 학습

Day 64: 이터레이터와 제너레이터 심화 ⭐⭐⭐

내일은 이터레이터 프로토콜, 커스텀 이터레이터, 그리고 고급 제너레이터 기법을 배워요!


“메모리를 아끼면 프로그램이 빨라져요!” 🚀

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