Python & AI Tutorials Logo
Python 프로그래밍

36. 제너레이터(generator)와 지연 반복(lazy iteration)

35장에서는 이터러블(iterable)과 이터레이터(iterator)를 통해 Python에서 반복이 어떻게 작동하는지 배웠습니다. 이터레이터는 요청받을 때마다 한 번에 하나씩 값을 반환하므로, Python이 모든 것을 한 번에 메모리에 로드하지 않고도 시퀀스를 처리할 수 있다는 것을 보았습니다. 이제 이터레이터를 만드는 Python의 가장 우아하고 실용적인 방법인 제너레이터(generator) 살펴보겠습니다.

제너레이터는 실행을 일시 중지하고 재개할 수 있는 함수로, 모든 값을 미리 계산하여 메모리에 저장하는 대신, 요청받을 때마다 한 번에 하나씩 값을 생성합니다. 이러한 접근 방식—지연 평가(lazy evaluation)라고 불립니다—은 필요할 때만 값을 생성한다는 의미이며, 메모리 효율적인 코드를 작성하기 위한 Python의 가장 강력한 기능 중 하나입니다.

36.1) 제너레이터가 무엇이며 왜 유용한가

36.1.1) 큰 리스트(list)를 만들 때의 문제

먼저 제너레이터가 해결하는 문제를 이해해 봅시다. 100만 개의 숫자 시퀀스(sequence)를 처리해야 한다고 가정해 보겠습니다. 다음은 리스트(list)를 사용하는 전통적인 접근입니다:

python
# 100만 개의 제곱을 담은 리스트 만들기
def get_squares_list(n):
    """0부터 n-1까지의 제곱 리스트를 반환합니다."""
    squares = []
    for i in range(n):
        squares.append(i * i)
    return squares
 
# 이것은 메모리에 1,000,000개의 숫자를 담은 리스트를 생성합니다
numbers = get_squares_list(1_000_000)
print(f"First five squares: {numbers[:5]}")  # Output: First five squares: [0, 1, 4, 9, 16]

이 접근에는 중요한 문제가 있습니다. 한 번에 하나씩 처리하기만 해도 되는데도, 100만 개의 숫자 전부를 한꺼번에 메모리에 만들고 저장합니다. 더 큰 데이터셋이거나 계산이 더 복잡하다면, 이는 엄청난 메모리를 소비하거나 심지어 프로그램이 크래시(crash) 날 수도 있습니다.

36.1.2) 제너레이터 소개: 필요할 때 값 계산하기

제너레이터(generator) 는 요청될 때만 한 번에 하나씩 값을 생성하는 특별한 종류의 함수(function)입니다. 완전한 리스트(list)를 만들고 반환하는 대신, 제너레이터는 필요할 때마다 각 값을 계산하고 호출 사이에 어디까지 했는지 "기억"합니다.

다음은 동일한 기능을 제너레이터(generator)로 구현한 예입니다:

python
# 제곱을 생성하는 제너레이터 만들기
def get_squares_generator(n):
    """0부터 n-1까지의 제곱을 한 번에 하나씩 생성합니다."""
    for i in range(n):
        yield i * i  # yield는 함수를 일시 중지하고 값을 반환합니다
 
# 이것은 리스트가 아니라 제너레이터 객체를 생성합니다
squares_gen = get_squares_generator(1_000_000)
print(squares_gen)  # Output: <generator object get_squares_generator at 0x...>
 
# 값을 한 번에 하나씩 가져오기
print(next(squares_gen))  # Output: 0
print(next(squares_gen))  # Output: 1
print(next(squares_gen))  # Output: 4

제너레이터는 100만 개의 제곱을 미리 모두 계산하지 않습니다. 대신 next()를 호출할 때마다 그 제곱을 하나씩 계산합니다. 호출 사이에 제너레이터는 "일시 중지"되고, 자신의 상태(현재 i 값)를 기억합니다.

36.1.3) 메모리 효율: 핵심 장점

리스트(list)와 제너레이터(generator) 사이의 메모리 차이는 큰 데이터셋에서 극적으로 나타납니다. 비교해 봅시다:

python
import sys
 
# 리스트 접근: 모든 값을 저장합니다
def squares_list(n):
    return [i * i for i in range(n)]
 
# 제너레이터 접근: 필요할 때 값을 계산합니다
def squares_generator(n):
    for i in range(n):
        yield i * i
 
# 100,000개 숫자에 대한 메모리 사용량 비교
list_result = squares_list(100_000)
gen_result = squares_generator(100_000)
 
print(f"List size in memory: {sys.getsizeof(list_result):,} bytes")
# Output: List size in memory: 800,984 bytes (실제 크기는 다를 수 있음)
 
print(f"Generator size in memory: {sys.getsizeof(gen_result)} bytes")
# Output: Generator size in memory: 200 bytes (실제 크기는 다를 수 있음)

리스트는 800KB가 넘는 메모리를 소비하지만, 제너레이터는 200바이트만 사용합니다—얼마나 많은 값을 생성하든 상관없이 말입니다. 제너레이터는 값 시퀀스(sequence) 자체가 아니라 함수의 상태(현재 i 값과 어디서 재개할지)만 저장합니다.

36.1.4) 제너레이터가 유용한 경우

제너레이터는 여러 흔한 상황에서 뛰어난 성능을 보입니다:

큰 파일 처리:

python
def read_large_file(filename):
    """파일에서 한 번에 한 줄씩 생성합니다."""
    with open(filename, 'r') as file:
        for line in file:
            yield line.strip()
 
# 거대한 로그 파일을 메모리에 전부 올리지 않고 처리하기
for line in read_large_file('huge_log.txt'):
    if 'ERROR' in line:
        print(line)

무한 시퀀스(sequence):

python
def fibonacci():
    """피보나치 수를 무한히 생성합니다."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
 
# 피보나치 수를 영원히 생성하기(또는 더 이상 요청하지 않을 때까지)
fib = fibonacci()
print([next(fib) for _ in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

36.1.5) 제너레이터는 이터레이터(iterator)다

35장에서 배웠듯이 제너레이터(generator)는 사실 특별한 종류의 이터레이터(iterator)입니다. 제너레이터는 이터레이터 프로토콜(iterator protocol)(__iter__()__next__())을 자동으로 구현하므로, for 반복문(loop)과 매끄럽게 동작합니다:

python
def countdown(n):
    """n부터 1까지 카운트다운을 생성합니다."""
    while n > 0:
        yield n
        n -= 1
 
# 제너레이터는 for 반복문에서 바로 동작합니다
for num in countdown(5):
    print(num)
# Output:
# 5
# 4
# 3
# 2
# 1

제너레이터를 for 반복문(loop)에서 사용하면, Python은 제너레이터가 소진되어 StopIteration을 발생시킬 때까지 자동으로 반복해서 next()를 호출합니다.

36.2) yield로 제너레이터 함수 만들기

36.2.1) yield 문(statement): 일시 중지와 재개

yield 문(statement)은 함수를 제너레이터(generator)로 만들어 줍니다. Python이 yield를 만나면 특별한 일을 합니다. 값을 반환하고 함수를 종료하는 대신, 함수 실행을 일시 중지하고 그 값을 반환합니다. 다음에 제너레이터에 next()를 호출하면, 실행은 yield 문 바로 다음부터 다시 시작됩니다.

다음은 이 일시 중지/재개 동작을 보여 주는 간단한 예제입니다:

python
def simple_generator():
    """yield가 실행을 어떻게 일시 중지하는지 보여 줍니다."""
    print("Starting generator")
    yield 1
    print("Resuming after first yield")
    yield 2
    print("Resuming after second yield")
    yield 3
    print("Generator finished")
 
gen = simple_generator()
print("Created generator")
# Output:
# Created generator
 
print(f"First value: {next(gen)}")
# Output:
# Starting generator
# First value: 1
 
print(f"Second value: {next(gen)}")
# Output:
# Resuming after first yield
# Second value: 2
 
print(f"Third value: {next(gen)}")
# Output:
# Resuming after second yield
# Third value: 3
 
try:
    next(gen)
except StopIteration:
    print("Generator exhausted - no more values")
# Output:
# Generator finished
# Generator exhausted - no more values

함수의 실행이 next() 호출과 번갈아(interleaved) 일어나는 것을 확인할 수 있습니다. 각 yield는 함수를 일시 중지하고, 각 next()는 중단된 지점부터 다시 실행합니다.

36.2.2) 제너레이터 상태(state): 로컬 변수 기억하기

제너레이터는 yield 사이에 모든 지역 변수를 기억합니다. 이를 통해 여러 호출에 걸쳐 상태를 유지할 수 있습니다:

python
def counter(start=0):
    """start부터 시작하는 순차 숫자 생성."""
    current = start
    while True:
        yield current
        current += 1
 
# Generator는 yield 사이에 'current'를 기억합니다
count = counter(10)
print(next(count))  # Output: 10
print(next(count))  # Output: 11
print(next(count))  # Output: 12
 
# 각 generator는 독립적인 상태를 가집니다
count1 = counter(0)
count2 = counter(100)
print(next(count1))  # Output: 0
print(next(count2))  # Output: 100
print(next(count1))  # Output: 1
print(next(count2))  # Output: 101

변수 current는 제너레이터가 yield에서 일시 중지하고 다음 next() 호출에서 재개될 때마다 보존됩니다. 이를 통해 제너레이터가 마지막 값부터 계속 카운팅할 수 있습니다. 각 제너레이터 인스턴스는 자신만의 독립적인 상태를 유지합니다.

36.2.3) 반복문(loop)에서 yield하기: 가장 흔한 패턴

제너레이터의 가장 흔한 사용법은 반복문(loop) 안에서 값을 yield하는 것입니다. 이 패턴은 값의 시퀀스(sequence)를 생성합니다:

python
def even_numbers(start, end):
    """주어진 범위에서 짝수를 생성합니다."""
    current = start if start % 2 == 0 else start + 1
    while current <= end:
        yield current
        current += 2
 
# 제너레이터 사용하기
evens = even_numbers(1, 20)
print(list(evens))
# Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

반복문의 각 반복(iteration)은 값 하나를 yield하고, 다음에 다시 next()가 호출되면 다음 반복으로 진행합니다.

36.2.4) 여러 개의 yield 문(statement)

제너레이터는 코드의 서로 다른 지점에 여러 개의 yield 문(statement)을 가질 수 있습니다. 실행은 순서대로 흘러갑니다:

python
def process_data(data):
    """상태 메시지와 함께 처리된 데이터를 생성합니다."""
    yield "Starting processing..."
    
    cleaned = [item.strip().lower() for item in data]
    yield f"Cleaned {len(cleaned)} items"
    
    unique = list(set(cleaned))
    yield f"Found {len(unique)} unique items"
    
    for item in sorted(unique):
        yield item
 
# 데이터 처리하기
data = ["  Apple  ", "Banana", "apple", "Cherry", "BANANA"]
processor = process_data(data)
 
for result in processor:
    print(result)
# Output:
# Starting processing...
# Cleaned 5 items
# Found 3 unique items
# apple
# banana
# cherry

이 패턴은 제너레이터가 설정 작업을 수행하고, 상태 정보를 yield한 뒤, 실제 데이터를 yield해야 할 때 유용합니다.

36.3) 제너레이터 표현식 vs 리스트 컴프리헨션

36.3.1) 제너레이터 표현식 소개

34장에서 리스트 컴프리헨션(list comprehensions)을 배웠습니다. 이는 리스트(list)를 간결하게 만드는 방법입니다. 제너레이터 표현식(generator expressions) 은 거의 동일한 문법을 사용하지만 리스트 대신 제너레이터를 만듭니다.

제너레이터 표현식은 기본적으로 간단한 제너레이터 함수를 작성하는 간결한 방법입니다. 다음 두 가지 접근 방식을 비교해봅시다:

python
# Generator 함수
def squares_function(n):
    for x in range(n):
        yield x * x
 
# Generator expression - 같은 일을 합니다
squares_expression = (x * x for x in range(10))
 
# 둘 다 generator 객체를 생성합니다
gen1 = squares_function(10)
gen2 = squares_expression
 
print(type(gen1))  # Output: <class 'generator'>
print(type(gen2))  # Output: <class 'generator'>
 
# 둘 다 같은 값을 생성합니다
print(list(squares_function(10)))  # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_expression))  # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

문법은 리스트 컴프리헨션과 거의 동일합니다. 차이점은 대괄호 [] 대신 괄호 ()를 사용한다는 것과, 리스트 컴프리헨션은 리스트를 만들고, 제너레이터 표현식은 제너레이터를 만든다는 것입니다:

python
# 리스트 컴프리헨션 - 전체 리스트를 메모리에 생성합니다
squares_list = [x * x for x in range(10)]
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
 
# 제너레이터 표현식 - 제너레이터 객체를 생성합니다
squares_gen = (x * x for x in range(10))
print(squares_gen)
# Output: <generator object <genexpr> at 0x...>
 
# 값을 보기 위해 리스트로 변환하기
print(list(squares_gen))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

제너레이터 표현식은 리스트 컴프리헨션과 같은 간결한 문법을 제공하면서도 제너레이터의 메모리 효율을 가집니다.

36.3.2) 메모리 비교: 중요해지는 순간

작은 시퀀스(sequence)에서는 리스트 컴프리헨션과 제너레이터 표현식 사이의 메모리 차이가 무시할 만합니다. 하지만 큰 시퀀스에서는 차이가 크게 납니다:

python
import sys
 
# 작은 시퀀스 - 차이가 거의 없습니다
small_list = [x for x in range(100)]
small_gen = (x for x in range(100))
 
print(f"Small list: {sys.getsizeof(small_list)} bytes")
# Output: Small list: 920 bytes (실제 크기는 다를 수 있음)
print(f"Small generator: {sys.getsizeof(small_gen)} bytes")
# Output: Small generator: 192 bytes (실제 크기는 다를 수 있음)
 
# 큰 시퀀스 - 차이가 큽니다
large_list = [x for x in range(1_000_000)]
large_gen = (x for x in range(1_000_000))
 
print(f"Large list: {sys.getsizeof(large_list):,} bytes")
# Output: Large list: 8,448,728 bytes (실제 크기는 다를 수 있음)
print(f"Large generator: {sys.getsizeof(large_gen)} bytes")
# Output: Large generator: 192 bytes (실제 크기는 다를 수 있음)

제너레이터의 크기는 생성할 값의 개수와 관계없이 일정합니다—표현식과 현재 상태만 저장합니다. 반면 리스트는 모든 값을 메모리에 저장해야 하므로, 요소 수에 비례하여 크기가 증가합니다.

36.3.3) 함수 호출에서의 제너레이터 표현식

제너레이터 표현식은 이터러블(iterable)을 소비하는 함수에 직접 전달될 때 특히 우아합니다. 제너레이터 표현식이 유일한 인자라면 추가 괄호를 생략할 수 있습니다:

python
# 리스트를 만들지 않고 제곱의 합 계산하기
total = sum(x * x for x in range(100))  # Note: no extra parentheses needed
print(total)
# Output: 328350
 
# 변환된 값의 최댓값 찾기
numbers = [1, 2, 3, 4, 5]
max_square = max(x * x for x in numbers)
print(max_square)
# Output: 25
 
# 어떤 값이라도 조건을 만족하는지 확인하기
data = [10, 15, 20, 25, 30]
has_large = any(x > 100 for x in data)
print(has_large)
# Output: False

이 패턴은 메모리 효율적이면서도 읽기 좋습니다. sum(), max(), min(), any(), all() 같은 함수는 제너레이터를 한 번에 하나의 값씩 처리하며, 중간 리스트를 절대 만들지 않습니다.

36.3.4) 제너레이터 표현식으로 필터링하기

제너레이터 표현식은 리스트 컴프리헨션과 동일한 조건 로직을 지원합니다:

python
# 짝수만 필터링하기
numbers = range(20)
evens = (x for x in numbers if x % 2 == 0)
print(list(evens))
# Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
 
# 변환과 필터링
words = ["hello", "world", "python", "programming"]
long_upper = (word.upper() for word in words if len(word) > 5)
print(list(long_upper))
# Output: ['PYTHON', 'PROGRAMMING']

36.3.5) 제너레이터 표현식만으로는 부족할 때

제너레이터 표현식은 간결하고 우아하지만 한계가 있습니다. 다음이 필요할 때는 제너레이터 함수(function)를 사용하세요:

복잡한 로직:

python
# 제너레이터 표현식에는 너무 복잡합니다
def process_log_lines(filename):
    """복잡한 로직으로 로그 파일을 처리합니다."""
    with open(filename, 'r') as file:
        for line in file:
            line = line.strip()
            if not line or line.startswith('#'):
                continue  # 빈 줄과 주석을 건너뜁니다
            
            parts = line.split('|')
            if len(parts) >= 3:
                timestamp, level, message = parts[0], parts[1], parts[2]
                if level in ('ERROR', 'CRITICAL'):
                    yield {
                        'timestamp': timestamp,
                        'level': level,
                        'message': message
                    }

여러 번의 yield 또는 상태(state):

python
# 제너레이터 표현식은 반복 사이에서 상태를 유지할 수 없습니다
def running_total(numbers):
    """숫자의 누적 합을 생성합니다."""
    total = 0
    for num in numbers:
        total += num
        yield total
 
numbers = [1, 2, 3, 4, 5]
print(list(running_total(numbers)))
# Output: [1, 3, 6, 10, 15]

에러 처리:

python
# 제너레이터 표현식은 예외를 처리할 수 없습니다
def safe_divide(numbers, divisor):
    """에러를 처리하면서 나눗셈 결과를 생성합니다."""
    for num in numbers:
        try:
            yield num / divisor
        except ZeroDivisionError:
            yield float('inf')

36.4) 리스트 대신 제너레이터를 사용해야 할 때

36.4.1) 대용량 데이터셋: 주요 사용 사례

제너레이터를 사용하는 가장 설득력 있는 이유는 대용량 데이터를 처리할 때입니다. 수백만 개의 레코드를 처리하는 경우, 제너레이터는 원활하게 실행되는 프로그램과 충돌하는 프로그램의 차이를 만들 수 있습니다.

나쁜 접근 - 전체 파일을 메모리에 로드:

python
# 대용량 파일에는 이렇게 하지 마세요
def count_errors_bad(filename):
    """전체 파일을 메모리에 로드 - 대용량 파일에서 충돌."""
    with open(filename, 'r') as file:
        lines = file.readlines()  # 전체 파일을 메모리에 로드
    
    error_count = 0
    for line in lines:
        if 'ERROR' in line:
            error_count += 1
    
    return error_count
 
# 파일이 10 GB라면, 10 GB를 메모리에 로드하려고 시도합니다!

좋은 접근 - 제너레이터 사용:

python
def read_log_lines(filename):
    """로그 파일에서 한 번에 한 줄씩 생성."""
    with open(filename, 'r') as file:
        for line in file:
            yield line.strip()
 
def count_errors_good(filename):
    """전체 파일을 메모리에 로드하지 않고 에러 개수 세기."""
    error_count = 0
    for line in read_log_lines(filename):
        if 'ERROR' in line:
            error_count += 1
    
    return error_count
 
# 기가바이트 크기의 로그 파일에서도 효율적으로 작동
# 한 번에 한 줄만 메모리에 유지하기 때문
count = count_errors_good('huge_application.log')
print(f"{count}개의 에러 발견")

제너레이터 접근 방식은 한 번에 한 줄씩 처리하므로, 파일 크기와 관계없이 메모리 사용량이 일정합니다. 10 GB 파일이나 10 KB 파일이나 같은 양의 메모리를 사용합니다.

36.4.2) 무한 또는 길이를 알 수 없는 시퀀스(sequence)

제너레이터는 길이를 미리 알 수 없거나, 개념적으로 무한한 시퀀스(sequence)에 완벽합니다:

python
def user_input_stream():
    """사용자가 'quit'을 입력할 때까지 사용자 입력을 생성합니다."""
    while True:
        user_input = input("Enter a number (or 'quit'): ")
        if user_input.lower() == 'quit':
            break
        try:
            yield int(user_input)
        except ValueError:
            print("Invalid number, try again")
 
# 들어오는 즉시 사용자 입력을 처리하기
total = 0
count = 0
for number in user_input_stream():
    total += number
    count += 1
    print(f"Running average: {total / count:.2f}")

길이를 모르는 리스트(list)는 만들 수 없지만, 제너레이터는 이를 자연스럽게 처리합니다.

36.4.3) 체인 변환: 데이터 파이프라인 구축

데이터에 여러 변환을 적용해야 할 때, 제너레이터를 사용하면 중간 리스트를 생성하지 않고 연산을 체인으로 연결할 수 있습니다:

python
# 여러 단계를 거쳐 숫자 변환
def generate_numbers(n):
    """1부터 n까지 숫자 생성."""
    for i in range(1, n + 1):
        yield i
 
def square_numbers(numbers):
    """입력 숫자들의 제곱 생성."""
    for num in numbers:
        yield num * num
 
def keep_even(numbers):
    """짝수만 생성."""
    for num in numbers:
        if num % 2 == 0:
            yield num
 
# Generator 체인 - 중간 리스트 생성 안 됨
numbers = generate_numbers(10)
squared = square_numbers(numbers)
even_squares = keep_even(squared)
 
# 결과 처리
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]

각 단계는 한 번에 하나의 값을 처리하여 다음 단계로 전달합니다. 이는 메모리 효율적이며, 사용 가능한 RAM보다 큰 데이터셋을 처리할 수 있게 합니다.

generate_numbers

square_numbers

keep_even

결과

제너레이터 없이는 중간 리스트가 필요합니다:

python
# 제너레이터 없는 접근 - 중간 리스트 생성
numbers = list(range(1, 11))           # [1, 2, 3, ..., 10]
squared = [n * n for n in numbers]     # [1, 4, 9, ..., 100]
even_squares = [n for n in squared if n % 2 == 0]  # [4, 16, 36, 64, 100]
 
# 제너레이터 사용 - 중간 리스트 없음
numbers = (i for i in range(1, 11))
squared = (n * n for n in numbers)
even_squares = (n for n in squared if n % 2 == 0)
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]

세 단계로 구성된 파이프라인이 백만 개의 항목을 처리하는 경우, 리스트 방식은 각각 백만 개의 항목을 가진 세 개의 리스트를 생성합니다. 제너레이터 방식은 한 번에 하나의 값만 메모리에 유지합니다.

36.4.4) 제너레이터보다 리스트(list)가 더 나은 경우

장점에도 불구하고 제너레이터가 언제나 정답은 아닙니다. 다음이 필요하다면 리스트(list)를 사용하세요:

여러 번의 반복(iteration):

python
# 리스트 - 여러 번 순회 가능
numbers = [1, 2, 3, 4, 5]
print(sum(numbers))      # Output: 15
print(max(numbers))      # Output: 5 (문제없음)
 
# Generator - 한 번만 순회 가능
numbers_gen = (x for x in range(1, 6))
print(sum(numbers_gen))  # Output: 15
print(max(numbers_gen))  # Output: ValueError: max() iterable argument is empty

같은 데이터를 여러 번 처리해야 한다면 리스트를 사용하세요.

임의 접근(random access):

python
# 인덱스로 요소에 접근해야 한다면 - 리스트 사용
students = ['Alice', 'Bob', 'Charlie', 'Diana']
print(students[2])  # Output: Charlie
 
# 제너레이터는 인덱싱을 지원하지 않습니다
students_gen = (name for name in students)
# students_gen[2]  # ERROR: 'generator' object is not subscriptable

길이 정보:

python
# 길이를 알아야 한다면 - 리스트 사용
data = [1, 2, 3, 4, 5]
print(f"Processing {len(data)} items")
 
# 제너레이터에는 길이가 없습니다
data_gen = (x for x in data)
# len(data_gen)  # ERROR: object of type 'generator' has no len()

작은 데이터셋:

python
# 작은 데이터셋에서는 리스트로도 충분하고 더 편리합니다
small_data = [x * 2 for x in range(10)]
 
# 여기서는 제너레이터의 메모리 절약이 그다지 크지 않고
# 리스트가 더 유연합니다

36.4.5) 실용적인 결정 가이드

다음은 제너레이터(generator)와 리스트(list) 사이에서 선택하기 위한 실용적인 가이드입니다:

제너레이터를 사용할 때:

  • 큰 파일이나 데이터셋을 처리할 때
  • 데이터 스트림(stream)이나 사용자 입력을 다룰 때
  • 데이터 처리 파이프라인(pipeline)을 만들 때
  • 메모리 효율이 중요할 때
  • 한 번만 반복(iteration)하면 될 때
  • 시퀀스(sequence)가 무한하거나 매우 길 때

리스트를 사용할 때:

  • 데이터셋이 작을 때(일반적으로 < 10,000개 항목)
  • 여러 번 반복(iteration)해야 할 때
  • 인덱스로 임의 접근(random access)이 필요할 때
  • 길이를 알아야 할 때
  • 리스트를 기대하는 코드에 데이터를 전달해야 할 때

36.4.6) 제너레이터와 리스트 사이 변환하기

필요할 때 제너레이터와 리스트 사이를 쉽게 변환할 수 있습니다:

python
# 제너레이터를 리스트로
numbers_gen = (x * 2 for x in range(5))
numbers_list = list(numbers_gen)
print(numbers_list)
# Output: [0, 2, 4, 6, 8]
 
# 리스트를 제너레이터로(제너레이터 표현식 사용)
numbers_list = [1, 2, 3, 4, 5]
numbers_gen = (x for x in numbers_list)

이러한 유연성은 효율성을 위해 generator로 시작하고 리스트 전용 기능이 필요할 때만 리스트로 변환할 수 있다는 것을 의미합니다:

python
# 메모리 효율성을 위해 generator로 시작
numbers = (x for x in range(1, 1001))
filtered = (x for x in numbers if x % 7 == 0)
 
# 여러 번 순회가 필요할 때 리스트로 변환
multiples_of_seven = list(filtered)
 
# 이제 리스트 기능 사용 가능
print(f"개수: {len(multiples_of_seven)}")
# Output: 개수: 142
 
print(f"첫 번째: {multiples_of_seven[0]}")
# Output: 첫 번째: 7
 
print(f"마지막: {multiples_of_seven[-1]}")
# Output: 마지막: 994
 
# 여러 번 순회 가능
total = sum(multiples_of_seven)
average = total / len(multiples_of_seven)
print(f"평균: {average:.1f}")
# Output: 평균: 500.5

제너레이터(generator)는 메모리 효율적인 코드를 작성하기 위한 Python의 가장 우아한 기능 중 하나입니다. 제너레이터는 큰 데이터셋을 처리하고, 데이터 파이프라인을 만들고, 무한 시퀀스(sequence)와도 작업할 수 있게 해주면서도—코드를 깔끔하고 읽기 쉽게 유지해 줍니다. 경험이 쌓이면 언제 제너레이터가 적합한 도구인지에 대한 직관이 생길 것입니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai