들어가며
제너레이터는 메모리 효율적인 데이터 처리를 가능하게 합니다. 대용량 데이터를 다룰 때 제너레이터를 사용하면 성능이 크게 개선됩니다.
제너레이터의 기본
예제 1: 기본 제너레이터
# 일반 함수
def count_up_to_list(n):
"""n까지의 수를 리스트로 반환"""
result = []
for i in range(1, n + 1):
result.append(i)
return result
# 제너레이터 함수
def count_up_to_generator(n):
"""n까지의 수를 제너레이터로 반환"""
for i in range(1, n + 1):
yield i
# 비교
print("리스트 방식:")
result_list = count_up_to_list(5)
print(f" 결과: {result_list}")
print(f" 타입: {type(result_list)}")
print("\n제너레이터 방식:")
result_gen = count_up_to_generator(5)
print(f" 타입: {type(result_gen)}")
print(f" 값들: {list(result_gen)}")
출력:
리스트 방식:
결과: [1, 2, 3, 4, 5]
타입: <class 'list'>
제너레이터 방식:
타입: <class 'generator'>
값들: [1, 2, 3, 4, 5]
예제 2: yield와 제너레이터 동작
def simple_generator():
print("제너레이터 시작")
yield 1
print("첫 번째 이후")
yield 2
print("두 번째 이후")
yield 3
gen = simple_generator()
print("제너레이터 생성 - 아직 실행 안 됨")
print("\nfirst next():")
value = next(gen)
print(f"값: {value}")
print("\nsecond next():")
value = next(gen)
print(f"값: {value}")
print("\nthird next():")
value = next(gen)
print(f"값: {value}")
# print("\nfourth next():")
# next(gen) # StopIteration 예외
출력:
제너레이터 생성 - 아직 실행 안 됨
first next():
제너레이터 시작
값: 1
second next():
첫 번째 이후
값: 2
third next():
두 번째 이후
값: 3
예제 3: 제너레이터와 for 루프
def fibonacci(n):
"""피보나치 수열을 n개 생성"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# for 루프에서 사용
print("첫 10개의 피보나치 수:")
for num in fibonacci(10):
print(num, end=' ')
print()
# 리스트로 변환
fib_list = list(fibonacci(10))
print(f"\n리스트: {fib_list}")
출력:
첫 10개의 피보나치 수:
0 1 1 2 3 5 8 13 21 34
리스트: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
제너레이터 표현식
예제 4: 제너레이터 표현식 vs 리스트 컴프리헨션
import sys
# 리스트 컴프리헨션 (메모리 많이 사용)
squares_list = [x**2 for x in range(10)]
print(f"리스트 크기: {sys.getsizeof(squares_list)} bytes")
# 제너레이터 표현식 (메모리 절약)
squares_gen = (x**2 for x in range(10))
print(f"제너레이터 크기: {sys.getsizeof(squares_gen)} bytes")
# 결과는 같음
print(f"\n리스트: {squares_list}")
print(f"제너레이터: {list(squares_gen)}")
# 대용량 데이터 처리
print(f"\n대용량 데이터 처리:")
big_list = [x**2 for x in range(1_000_000)]
print(f"1백만 요소 리스트: {sys.getsizeof(big_list)} bytes")
big_gen = (x**2 for x in range(1_000_000))
print(f"1백만 요소 제너레이터: {sys.getsizeof(big_gen)} bytes")
출력:
리스트 크기: 200 bytes
제너레이터 크기: 120 bytes
리스트: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
제너레이터: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
대용량 데이터 처리:
1백만 요소 리스트: 9000096 bytes
1백만 요소 제너레이터: 120 bytes
효율적 코딩 패턴
예제 5: 파일 처리 - 효율적 방식
# 파일 데이터 생성
with open("large_data.txt", "w") as f:
for i in range(10):
f.write(f"Line {i+1}: {'x'*100}\n")
# 비효율적: 전체 파일을 메모리에 로드
def read_file_inefficient(filename):
with open(filename, 'r') as file:
lines = file.readlines() # 전체 파일을 메모리에 로드
return lines
# 효율적: 한 줄씩 처리
def read_file_efficient(filename):
with open(filename, 'r') as file:
for line in file:
yield line.rstrip()
# 비교
print("비효율적 방식 - 전체 로드:")
lines = read_file_inefficient("large_data.txt")
print(f" 메모리 사용: {len(lines)} 줄 로드됨")
print("\n효율적 방식 - 한 줄씩 처리:")
count = 0
for line in read_file_efficient("large_data.txt"):
count += 1
print(f" 처리된 줄 수: {count}줄 (필요할 때만 로드)")
출력:
비효율적 방식 - 전체 로드:
메모리 사용: 10 줄 로드됨
효율적 방식 - 한 줄씩 처리:
처리된 줄 수: 10줄 (필요할 때만 로드)
예제 6: itertools 모듈 사용
import itertools
# 무한 수열 생성
def infinite_counter(start=0):
while True:
yield start
start += 1
# itertools.count 사용
counter = itertools.count(1)
first_5 = list(itertools.islice(counter, 5))
print(f"처음 5개: {first_5}")
# itertools.chain - 여러 이터러블 연결
numbers = [1, 2, 3]
letters = ['a', 'b', 'c']
combined = list(itertools.chain(numbers, letters))
print(f"연결: {combined}")
# itertools.combinations - 조합
items = ['A', 'B', 'C']
combinations = list(itertools.combinations(items, 2))
print(f"조합: {combinations}")
# itertools.cycle - 반복
cycle = itertools.cycle(['red', 'green', 'blue'])
colors = list(itertools.islice(cycle, 9))
print(f"반복: {colors}")
출력:
처음 5개: [1, 2, 3, 4, 5]
연결: [1, 2, 3, 'a', 'b', 'c']
조합: [('A', 'B'), ('A', 'C'), ('B', 'C')]
반복: ['red', 'green', 'blue', 'red', 'green', 'blue', 'red', 'green', 'blue']
성능 비교
예제 7: 성능 측정
import time
# 큰 범위 생성
def measure_list(n):
start = time.time()
result = [x**2 for x in range(n)]
elapsed = time.time() - start
return elapsed, len(result)
def measure_generator(n):
start = time.time()
result = (x**2 for x in range(n))
elapsed = time.time() - start
return elapsed, len(list(result))
def measure_sum_list(n):
start = time.time()
result = sum([x**2 for x in range(n)])
elapsed = time.time() - start
return elapsed, result
def measure_sum_generator(n):
start = time.time()
result = sum(x**2 for x in range(n))
elapsed = time.time() - start
return elapsed, result
# 성능 테스트
n = 1_000_000
print("1백만 요소 처리:")
t, _ = measure_list(n)
print(f" 리스트 컴프리헨션: {t:.4f}초")
t, _ = measure_generator(n)
print(f" 제너레이터 표현식: {t:.4f}초")
print("\n합계 계산:")
t, sum_val = measure_sum_list(n)
print(f" 리스트: {t:.4f}초")
t, sum_val = measure_sum_generator(n)
print(f" 제너레이터: {t:.4f}초")
출력:
1백만 요소 처리:
리스트 컴프리헨션: 0.0523초
제너레이터 표현식: 0.0001초
합계 계산:
리스트: 0.0812초
제너레이터: 0.0634초
흔한 실수와 해결책
실수 1: 제너레이터 재사용
틀린 예제:
gen = (x**2 for x in range(5))
list1 = list(gen)
list2 = list(gen) # 빈 리스트 - 제너레이터는 소진됨!
올바른 예제:
def create_squares():
return (x**2 for x in range(5))
list1 = list(create_squares())
list2 = list(create_squares())
실수 2: yield와 return 혼동
틀린 예제:
def bad_generator():
yield 1
return [2, 3] # 이 리스트는 반환되지 않음
for val in bad_generator():
print(val) # 1만 출력
올바른 예제:
def good_generator():
yield 1
yield 2
yield 3
for val in good_generator():
print(val) # 1, 2, 3 출력
연습 문제
문제 1: 범위 내 소수 생성
범위 내의 모든 소수를 생성하는 제너레이터를 작성하세요.
문제 2: 대용량 파일 처리
큰 파일을 읽어 특정 패턴의 줄들을 필터링하는 제너레이터를 작성하세요.
문제 3: 무한 시퀀스
무한 시퀀스를 생성하고 필요한 만큼만 꺼내는 함수를 작성하세요.
풀이
문제 1 풀이
def primes(limit):
"""limit 이하의 모든 소수를 생성"""
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
for num in range(2, limit + 1):
if is_prime(num):
yield num
# 사용
print("100 이하의 소수:")
print(list(primes(100)))
문제 2 풀이
def filter_lines(filename, pattern):
"""파일에서 패턴을 포함한 줄만 생성"""
with open(filename, 'r') as file:
for line in file:
if pattern in line:
yield line.rstrip()
# 사용
# for line in filter_lines("data.txt", "error"):
# print(line)
문제 3 풀이
def infinite_sequence(start=0, step=1):
"""무한 시퀀스 생성"""
current = start
while True:
yield current
current += step
# 사용
import itertools
gen = infinite_sequence(1)
first_10 = list(itertools.islice(gen, 10))
print(f"처음 10개: {first_10}")
마무리
제너레이터를 효과적으로 사용하면 메모리 효율성을 크게 개선할 수 있습니다. 대용량 데이터를 처리할 때는 항상 제너레이터를 고려하세요.