[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 예외 발생
어떻게 동작할까요? 🤔
next()호출하면 yield까지 실행해요- yield에서 값을 주고 일시정지해요
- 다음
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
💡 힌트
단계별 힌트:
- 2부터 n까지 반복해요
- 각 숫자가 소수인지 체크하는 함수(
is_prime)를 만들어요 - 소수면
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: 파일 필터링 제너레이터
텍스트 파일에서 특정 키워드를 포함한 줄만 반환하는 제너레이터를 작성하세요.
💡 힌트
단계별 힌트:
with open()으로 파일을 열어요enumerate(file, 1)로 줄 번호와 내용을 함께 가져와요- 키워드가 포함된 줄만
yield로 반환해요 - 대소문자 구분 없이 검색하려면
.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짜리 로그 파일도 메모리 부담 없이 처리해요
- 원하는 줄을 찾으면 바로 중단할 수 있어요!
📝 오늘 배운 내용 정리
- 제너레이터:
yield로 값을 하나씩 생성하는 메모리 효율적인 방법이에요 - 게으른 평가: 필요할 때만 값을 만들어요
- 제너레이터 표현식:
(x for x in range(10))- 리스트 컴프리헨션의 게으른 버전이에요 - 실무 활용: 대용량 파일, 무한 시퀀스, 데이터 파이프라인, 실시간 스트리밍
🔗 관련 자료
📚 이전 학습
Day 62: 데코레이터 심화 ⭐⭐⭐
어제는 매개변수가 있는 데코레이터, 클래스 데코레이터, 데코레이터 체이닝을 배웠어요!
📚 다음 학습
Day 64: 이터레이터와 제너레이터 심화 ⭐⭐⭐
내일은 이터레이터 프로토콜, 커스텀 이터레이터, 그리고 고급 제너레이터 기법을 배워요!
“메모리를 아끼면 프로그램이 빨라져요!” 🚀
Day 63/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
