deep-dive2025-01-10·8 min·232/348

Python Generator vs Iterator 차이점 완벽 비교

Python의 generator와 iterator의 차이점, 각각의 사용 사례, 성능 비교를 통해 올바른 선택 방법을 알아봅니다.

Python Generator vs Iterator 차이점 완벽 비교

Python에서 데이터를 순회하는 두 가지 주요 방법: iterator와 generator. 비슷해 보이지만 각각의 특성과 사용 사례가 다릅니다.

Environment

$ python --version
Python 3.11.5

$ python -c "import sys; print(f'System memory: {sys.getsizeof([])} bytes for empty list')"
System memory: 56 bytes for empty list

Problem: 메모리 효율성 문제

큰 데이터셋을 처리할 때 iterator와 generator의 차이를 이해하지 못하면 메모리 문제가 발생할 수 있습니다:

import sys

# List comprehension - 모든 데이터를 메모리에 로드
def get_squares_list(n):
    return [x * x for x in range(n)]

# Generator expression - 필요할 때마다 생성
def get_squares_generator(n):
    return (x * x for x in range(n))

# Compare memory usage
list_data = get_squares_list(1000000)
gen_data = get_squares_generator(1000000)

print(f"List size: {sys.getsizeof(list_data)} bytes")
print(f"Generator size: {sys.getsizeof(gen_data)} bytes")

출력:

List size: 8448728 bytes
Generator size: 200 bytes

메모리 사용량이 42,000배 이상 차이가 납니다.

Analysis: Iterator Protocol 이해

Iterator란?

Iterator는 __iter____next__ 메서드를 가진 객체입니다:

class CountUp:
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

# Usage
counter = CountUp(1, 5)
for num in counter:
    print(num, end=" ")
# Output: 1 2 3 4

Generator란?

Generator는 iterator를 간단하게 만드는 문법입니다:

def count_up(start, end):
    current = start
    while current < end:
        yield current
        current += 1

# Usage - same behavior as iterator class
for num in count_up(1, 5):
    print(num, end=" ")
# Output: 1 2 3 4

Solution: Iterator와 Generator 비교

Iterator 구현

class FibonacciIterator:
    def __init__(self, max_count):
        self.max_count = max_count
        self.count = 0
        self.a = 0
        self.b = 1
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.count >= self.max_count:
            raise StopIteration
        
        value = self.a
        self.a, self.b = self.b, self.a + self.b
        self.count += 1
        return value

# Usage
fib_iter = FibonacciIterator(10)
fib_list = list(fib_iter)
print(f"Fibonacci: {fib_list}")
# Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator 구현

def fibonacci_generator(max_count):
    a, b = 0, 1
    count = 0
    while count < max_count:
        yield a
        a, b = b, a + b
        count += 1

# Usage
fib_gen = fibonacci_generator(10)
fib_list = list(fib_gen)
print(f"Fibonacci: {fib_list}")
# Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator Pipeline (체이닝)

def read_large_file(file_path):
    """Generator: 파일에서 한 줄씩 읽기"""
    with open(file_path, 'r') as f:
        for line in f:
            yield line.strip()

def filter_comments(lines):
    """Generator: 주석 줄 필터링"""
    for line in lines:
        if not line.startswith('#'):
            yield line

def extract_words(lines):
    """Generator: 단어 추출"""
    for line in lines:
        for word in line.split():
            yield word.lower()

# Pipeline - 각 단계가 lazy evaluation
def word_frequency(file_path):
    lines = read_large_file(file_path)
    non_comments = filter_comments(lines)
    words = extract_words(non_comments)
    
    freq = {}
    for word in words:
        freq[word] = freq.get(word, 0) + 1
    return freq

# Memory efficient processing of large files
freq = word_frequency("large_log_file.txt")

Performance Comparison

import time
import sys

def benchmark_list_approach(n):
    """List approach - all data in memory"""
    start = time.time()
    
    data = [x ** 2 for x in range(n)]
    total = sum(data)
    
    elapsed = time.time() - start
    memory = sys.getsizeof(data)
    
    return elapsed, memory

def benchmark_generator_approach(n):
    """Generator approach - lazy evaluation"""
    start = time.time()
    
    def squares():
        for x in range(n):
            yield x ** 2
    
    total = sum(squares())
    
    elapsed = time.time() - start
    memory = sys.getsizeof(squares())
    
    return elapsed, memory

n = 10_000_000
list_time, list_mem = benchmark_list_approach(n)
gen_time, gen_mem = benchmark_generator_approach(n)

print(f"List: {list_time:.3f}s, {list_mem:,} bytes")
print(f"Generator: {gen_time:.3f}s, {gen_mem} bytes")

출력:

List: 1.234s, 89,095,160 bytes
Generator: 0.892s, 200 bytes

Lessons Learned

  1. 메모리 효율성: Generator는 데이터를 한 번에 메모리에 로드하지 않으므로 대용량 데이터 처리에 적합합니다.

  2. Lazy Evaluation: Generator는 요청된 값만 생성하므로, 전체 시퀀스를 생성하지 않아도 됩니다.

  3. 한 번만 순회: Iterator와 Generator는 모두 한 번만 순회할 수 있습니다. 다시 순회하려면 새로 생성해야 합니다.

  4. 복잡한 로직에는 Iterator: 복잡한 상태 관리가 필요하면 Iterator 클래스가 더 명확할 수 있습니다.

  5. 간단한 변환에는 Generator: 간단한 데이터 변환 및 필터링에는 Generator가 더 간결하고 효율적입니다.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.