// education

제너레이터와 효율적 코딩

들어가며

제너레이터는 메모리 효율적인 데이터 처리를 가능하게 합니다. 대용량 데이터를 다룰 때 제너레이터를 사용하면 성능이 크게 개선됩니다.

제너레이터의 기본

예제 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}")

마무리

제너레이터를 효과적으로 사용하면 메모리 효율성을 크게 개선할 수 있습니다. 대용량 데이터를 처리할 때는 항상 제너레이터를 고려하세요.

← 이전데코레이터와 메타프로그래밍 다음 →실전 프로젝트 총정리