제너레이터란 무엇인가?
파이썬에서 대용량 데이터를 처리할 때 메모리 부족 문제를 겪어본 적이 있나요? 제너레이터(Generator)는 이러한 문제를 해결하는 강력한 도구입니다. 일반 함수가 return으로 값을 한 번에 반환하는 반면, 제너레이터는 yield를 사용해 값을 하나씩 생성하면서 메모리를 절약합니다.
제너레이터는 이터레이터를 생성하는 함수로, 필요할 때마다 값을 생성하여 메모리 효율성을 극대화합니다.
일반 함수 vs 제너레이터 비교
| 특징 | 일반 함수 | 제너레이터 |
|---|---|---|
| 반환 키워드 | return |
yield |
| 메모리 사용 | 모든 값을 메모리에 저장 | 한 번에 하나씩 생성 |
| 실행 방식 | 한 번 실행 후 종료 | 상태를 유지하며 중단/재개 |
| 반환 타입 | 값 또는 리스트 | 제너레이터 객체 |
| 대용량 데이터 처리 | 메모리 부족 위험 | 효율적 |
yield의 동작 원리
yield는 함수의 실행을 일시 중단하고 값을 호출자에게 반환합니다. 다음 호출 시 중단된 지점부터 재개됩니다.
def simple_generator():
print("첫 번째 값 생성")
yield 1
print("두 번째 값 생성")
yield 2
print("세 번째 값 생성")
yield 3
gen = simple_generator()
print(next(gen)) # 출력: 첫 번째 값 생성 \n 1
print(next(gen)) # 출력: 두 번째 값 생성 \n 2
print(next(gen)) # 출력: 세 번째 값 생성 \n 3
yield의 핵심 특징
- 지연 평가(Lazy Evaluation): 값이 실제로 필요할 때만 계산
- 상태 보존: 로컬 변수와 실행 위치를 기억
- 무한 시퀀스 생성 가능: 메모리 제약 없이 무한 데이터 스트림 생성
실전 예제 1: 대용량 파일 처리
수백만 줄의 로그 파일을 처리할 때 제너레이터가 얼마나 효율적인지 비교해봅시다.
# 비효율적인 방법 - 전체 파일을 메모리에 로드
def read_file_all(filename):
with open(filename, 'r') as f:
return f.readlines() # 100GB 파일이면 메모리 초과!
# 효율적인 방법 - 제너레이터 사용
def read_file_generator(filename):
with open(filename, 'r') as f:
for line in f:
yield line.strip()
# 사용 예시
for line in read_file_generator('huge_log.txt'):
if 'ERROR' in line:
print(line)
메모리 사용량 비교:
– 일반 방식: 파일 크기만큼 메모리 사용 (10GB 파일 = 10GB RAM)
– 제너레이터: 한 줄씩 처리 (수 KB 수준)
실전 예제 2: 피보나치 수열 무한 생성
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 처음 10개의 피보나치 수 출력
fib = fibonacci()
for _ in range(10):
print(next(fib))
# 출력: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
수학적으로 피보나치 수열의 번째 항 은 다음 재귀 관계식으로 정의됩니다:
여기서 , 입니다. 제너레이터는 이 수열을 메모리 제약 없이 무한히 생성할 수 있습니다.
실전 예제 3: 데이터 파이프라인 구축
여러 제너레이터를 연결하여 데이터 파이프라인을 만들 수 있습니다.
# 1단계: 데이터 읽기
def read_numbers(filename):
with open(filename, 'r') as f:
for line in f:
yield int(line.strip())
# 2단계: 짝수만 필터링
def filter_even(numbers):
for num in numbers:
if num % 2 == 0:
yield num
# 3단계: 제곱 계산
def square(numbers):
for num in numbers:
yield num ** 2
# 파이프라인 실행
pipeline = square(filter_even(read_numbers('numbers.txt')))
for result in pipeline:
print(result)
파이프라인의 장점
| 특징 | 설명 |
|---|---|
| 모듈화 | 각 단계를 독립적으로 테스트/수정 가능 |
| 메모리 효율 | 중간 결과를 저장하지 않음 |
| 가독성 | 데이터 흐름이 명확함 |
| 재사용성 | 단계별 함수를 다른 파이프라인에서 재사용 |
실전 예제 4: 배치 데이터 처리
머신러닝 학습 시 대용량 데이터를 배치로 나누어 처리하는 패턴입니다.
def batch_generator(data, batch_size):
"""데이터를 batch_size 크기로 나누어 반환"""
for i in range(0, len(data), batch_size):
yield data[i:i + batch_size]
# 사용 예시
data = range(1000000) # 100만 개의 데이터
for batch in batch_generator(data, batch_size=1000):
# 각 배치를 모델에 입력
process_batch(batch) # 1000개씩 처리
제너레이터 표현식
리스트 컴프리헨션과 유사하지만 [] 대신 ()를 사용합니다.
# 리스트 컴프리헨션 - 즉시 모든 값 생성
squares_list = [x**2 for x in range(1000000)] # 메모리 많이 사용
# 제너레이터 표현식 - 필요할 때 생성
squares_gen = (x**2 for x in range(1000000)) # 메모리 절약
print(sum(squares_gen)) # 값이 필요할 때만 계산
yield from: 서브 제너레이터 위임
yield from을 사용하면 다른 제너레이터의 값을 간단히 위임할 수 있습니다.
def generator1():
yield from range(3)
yield from range(3, 6)
# 위 코드는 아래와 동일
def generator2():
for i in range(3):
yield i
for i in range(3, 6):
yield i
print(list(generator1())) # [0, 1, 2, 3, 4, 5]
제너레이터 활용 패턴 정리
| 패턴 | 사용 사례 | 메모리 절감 효과 |
|---|---|---|
| 파일 스트리밍 | 대용량 로그/CSV 처리 | 파일 크기에 무관 |
| 무한 시퀀스 | 난수 생성, 시계열 데이터 | 무한대 |
| 데이터 파이프라인 | ETL, 데이터 전처리 | 중간 결과 제거 |
| 배치 처리 | ML 학습, API 페이지네이션 | 배치 크기만큼 |
| 필터링/변환 | 조건부 데이터 추출 | 불필요한 데이터 제거 |
성능 비교: 실측 데이터
1억 개의 숫자 중 짝수의 제곱을 구하는 작업을 비교했습니다.
import sys
# 리스트 사용
data_list = [x**2 for x in range(100000000) if x % 2 == 0]
print(f"리스트 메모리: {sys.getsizeof(data_list) / 1024 / 1024:.2f} MB")
# 출력: 약 400 MB
# 제너레이터 사용
data_gen = (x**2 for x in range(100000000) if x % 2 == 0)
print(f"제너레이터 메모리: {sys.getsizeof(data_gen) / 1024:.2f} KB")
# 출력: 약 0.1 KB
결과: 제너레이터는 리스트 대비 400만 배 적은 메모리를 사용합니다!
주의사항
- 일회성: 제너레이터는 한 번 순회하면 소진됩니다.
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] - 이미 소진됨
-
인덱싱 불가: 제너레이터는
gen[0]같은 인덱스 접근이 불가능합니다. -
길이 확인 불가:
len(gen)을 호출할 수 없습니다.
제너레이터는 스트림 데이터 처리에 최적화되어 있으며, 랜덤 액세스가 필요하면 리스트를 사용해야 합니다.
마무리
Python 제너레이터와 yield는 메모리 효율적인 프로그래밍의 핵심 도구입니다. 주요 활용 시나리오를 정리하면:
- 대용량 파일 처리: 파일 전체를 메모리에 로드하지 않고 한 줄씩 처리
- 무한 데이터 스트림: 피보나치, 난수 생성 등 끝없는 시퀀스 생성
- 데이터 파이프라인: 읽기-필터링-변환을 메모리 낭비 없이 연결
- 배치 처리: ML 학습이나 API 호출 시 데이터를 적절한 크기로 분할
제너레이터를 사용하면 400만 배까지 메모리를 절약할 수 있으며, 코드의 가독성과 모듈화도 향상됩니다. 다음 프로젝트에서 대용량 데이터를 다룬다면 yield를 적극 활용해보세요!
Did you find this helpful?
☕ Buy me a coffee
Leave a Reply