개발언어 Back-End/Python

컴프리헨션 / 제너레이터 정리 / 지연평가

bluebamus 2025. 2. 12.

1. 파이썬 컴프리헨션과 제너레이터의 차이 정리

구분 컴프리헨션 (Comprehension) 제너레이터 (Generator)
정의 리스트, 딕셔너리, 집합 등을 간결하게 생성하는 문법 이터레이터를 생성하는 함수 또는 표현식
사용 예시 [x**2 for x in range(10)] (x**2 for x in range(10))
리턴 타입 list, set, dict 등 제너레이터 객체 (generator)
메모리 사용 모든 데이터를 한 번에 메모리에 저장 데이터를 한 개씩 생성 (Lazy Evaluation)
속도 작은 데이터셋에서는 빠름 큰 데이터셋에서는 효율적
수정 가능 여부 리스트, 딕셔너리 등으로 저장되므로 수정 가능 한 번만 순회 가능하며 수정 불가
실행 방식 즉시 계산 후 결과 저장 필요할 때마다 next()로 값 생성
사용 목적 작은 데이터셋을 간결하게 만들 때 유용 대용량 데이터 처리 시 메모리 절약 가능

 

2. 리스트 컴프리헨션

   - 모든 요소를 한 번에 리스트에 저장함

lst = [x**2 for x in range(5)]
print(lst)  # [0, 1, 4, 9, 16]

 

3. 제너레이터 표현식

   - next()를 호출할 때마다 하나씩 값을 생성

gen = (x**2 for x in range(5))
print(next(gen))  # 0
print(next(gen))  # 1
print(list(gen))  # [4, 9, 16] (이미 소비된 값은 제외)

 

4. 언제 사용해야 할까?

   - 데이터가 크지 않고 빠른 접근이 필요하다면? :  컴프리헨션
   - 데이터가 많고 메모리를 아껴야 한다면? : 제너레이터

 

5. 제너레이터를 사용하면 무한한 크기의 데이터 스트림도 처리할 수 있음

   - 무한한 피보나치 수열 생성:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

gen = fibonacci()
for _ in range(5):
    print(next(gen))  # 0, 1, 1, 2, 3

 

6. 표현방식에 따른 컴프리헨션과 제너레이터 표현식의 구현 방법

표현 방식 컴프리헨션 제너레이터 표현식
리스트 [x**2 for x in range(n)] list((x**2 for x in range(n)))
{x % 3 for x in range(n)} set((x % 3 for x in range(n)))
딕셔너리 {x: x**2 for x in range(n)} dict(((x, x**2) for x in range(n)))
튜플 ❌ 없음 tuple((x**2 for x in range(n)))
next() 사용 ❌ 불가능 (리스트 반환) ✅ 가능 (메모리 절약)

 

7. 지연 평가(Lazy Evaluation)의 개념

   - 필요할 때까지 계산을 미루는 방식이다.
      - 즉, 어떤 표현식이나 함수의 결과값을 미리 계산하지 않고, 해당 결과가 실제로 필요할 때 계산하여 반환한다.
   - 이 방식은 메모리 사용량을 크게 줄여주며, 대용량 데이터 처리나 무한 시퀀스와 같이 전체 데이터를 한꺼번에 계산할 필요가 없을 때 유용하다.
      - 예를 들어, 리스트는 모든 값을 즉시 평가(eager evaluation)하여 메모리에 저장하지만, 제너레이터는 필요할 때마다 하나씩 값을 “생성(yield)”하여 메모리 사용을 최소화한다

 

8. 제너레이터를 통한 지연 평가의 동작 원리

   - 파이썬에서 제너레이터 함수는 yield 키워드를 사용하여 값을 반환하며, 함수의 실행 상태(state)를 내부에 저장한다.
      - 즉, 제너레이터 함수는 호출 시 즉시 모든 연산을 수행하지 않고, 첫 번째 yield 문에 도달할 때까지 실행되고 멈춘다.

   - 이후 호출자가 next() 또는 반복문(for 등)으로 제너레이터를 순회할 때마다 마지막으로 중단된 지점부터 다시 실행되어 새로운 값을 생성한다.

 

   1) 동작 순서 예시

      1. 제너레이터 함수 호출:

         - 제너레이터 함수가 호출되면 함수의 본문은 실행되지 않고 제너레이터 객체가 반환된다.

         - yield가 포함된 함수는 실행될 때마다 next() 호출을 통해 한 단계씩 진행

 

         - 동작 설명:

            - number_generator() 함수는 일반 함수처럼 보이지만, yield를 포함하므로 실행하면 제너레이터 객체가 생성된다.
            - 이 시점에서 함수 내부의 코드가 실행되지 않고, 단순히 제너레이터 객체만 반환된다.
            - 이후 next()를 호출해야 실제 실행이 시작된다.

# 제너레이터 함수 정의
def number_generator():
    print("제너레이터가 생성되었습니다.")
    yield 1
    yield 2
    yield 3

# 제너레이터 객체 생성 (이때는 코드가 실행되지 않음)
gen = number_generator()
print("제너레이터 객체 생성 완료:", gen)

 

     2. 첫 번째 next() 호출:

         - next(gen)을 호출하면 함수가 실행되기 시작하며, yield 키워드를 만날 때까지 진행된다.
         - yield를 만나면 해당 값을 반환하고, 실행이 멈춘 상태로 유지된다.

 

         - 동작 설명:

            - next(gen)을 호출하면 number_generator() 함수의 실행이 시작된다.
            - "제너레이터가 생성되었습니다."가 출력된다.
            - yield 1을 만나면 값 1이 반환되며, 함수 실행이 멈춘다(중단 상태).

print("첫 번째 next() 호출 결과:", next(gen))  # 첫 번째 yield 실행
제너레이터가 생성되었습니다.
첫 번째 next() 호출 결과: 1

 

      3. 이후의 next() 호출:

         - 제너레이터는 yield 이후의 코드 실행을 기억하고 있다가, next()가 호출될 때 중단된 위치에서 다시 실행된다.
         - 다시 yield를 만나면 값을 반환하고 멈춘다.

 

         - 동작 설명:

            - 두 번째 next(gen) 호출 시, 이전 yield 1이 실행된 이후 위치에서 다시 실행된다.
            - yield 2를 만나면 값 2가 반환되며, 다시 중단된다.
            - 세 번째 next(gen) 호출 시, yield 3을 만나 값 3이 반환된다.

print("두 번째 next() 호출 결과:", next(gen))  # 두 번째 yield 실행
print("세 번째 next() 호출 결과:", next(gen))  # 세 번째 yield 실행
두 번째 next() 호출 결과: 2
세 번째 next() 호출 결과: 3

 

      4. 모든 yield를 다 사용하면:

         - 제너레이터가 더 이상 반환할 값이 없으면 StopIteration 예외가 발생한다.
         - 이를 방지하기 위해 for 루프를 사용하면 자동으로 StopIteration을 처리할 수 있다.

print("네 번째 next() 호출 결과:", next(gen))  # 더 이상 반환할 값이 없음 -> 예외 발생
Traceback (most recent call last):
  ...
StopIteration

 

      - 이러한 동작 방식을 통해 제너레이터는 "순차적"으로 값들을 생성하며, 값이 필요할 때마다 계산이 이루어지므로 지연 평가가 실현된다.  

 

   2) 대량 데이터 처리 (파일 읽기)

      - 대용량 파일을 한 번에 로드하지 않고, 한 줄씩 읽어서 메모리를 절약할 수 있다.

def read_large_file(file_path):
    """파일을 한 줄씩 읽어오는 제너레이터"""
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()  # 한 줄씩 반환 (지연 평가)

# 제너레이터 활용 예시
for line in read_large_file("big_data.txt"):
    print(line)  # 한 줄씩 출력하여 메모리 절약

 

   3) 스트리밍 데이터 처리 (API 요청)

      - API 응답을 한 번에 처리하지 않고, 스트리밍 방식으로 부분적으로 받아서 처리 가능하다.
      - 대량의 데이터를 한꺼번에 메모리에 로드하지 않고 효율적으로 처리할 수 있다.

import requests

def fetch_data():
    """API 데이터를 스트리밍 방식으로 가져오는 제너레이터"""
    url = "https://api.example.com/data"
    response = requests.get(url, stream=True)

    for line in response.iter_lines():
        if line:
            yield line.decode("utf-8")  # 한 줄씩 반환

# 제너레이터 사용 예시
for data in fetch_data():
    print("받은 데이터:", data)

 

9. 코드 예제와 주석을 통한 상세 설명

   1) 간단한 제너레이터 함수 예제

      - 다음은 1부터 3까지의 정수를 하나씩 생성하는 제너레이터 함수이다.

 

      - 코드 설명:

         - 함수 simple_generator() 내의 각 print() 문은 해당 시점까지 함수가 실행되었음을 나타내며, yield 문에서 반환되는 값을 확인할 수 있다.
         - 각 next(gen) 호출 시마다 제너레이터는 이전 상태를 기억하면서 다음 yield 문까지 실행되므로, 데이터의 변화(1, 2, 3의 순차적 반환)를 관찰할 수 있다.

def simple_generator():
    # 첫 번째 값 생성 전: "simple_generator" 함수가 호출되어 제너레이터 객체가 생성됩니다.
    print("첫 번째 yield 직전")
    yield 1  # 첫 번째 next() 호출 시, 1을 반환하고 여기서 실행이 일시 중지됩니다.
    
    print("두 번째 yield 직전")
    yield 2  # 두 번째 next() 호출 시, 2를 반환하고 실행이 다시 중단됩니다.
    
    print("세 번째 yield 직전")
    yield 3  # 세 번째 next() 호출 시, 3을 반환합니다.
    
    print("함수 종료")  # 더 이상 yield할 값이 없으므로, 다음 next() 호출 시 StopIteration 예외가 발생합니다.

# 제너레이터 객체 생성 (실행은 아직 이루어지지 않음)
gen = simple_generator()

# 첫 번째 next() 호출: "첫 번째 yield 직전" 출력 후 1 반환
print("Next 1:", next(gen))  # 출력: 첫 번째 yield 직전 / Next 1: 1

# 두 번째 next() 호출: "두 번째 yield 직전" 출력 후 2 반환
print("Next 2:", next(gen))  # 출력: 두 번째 yield 직전 / Next 2: 2

# 세 번째 next() 호출: "세 번째 yield 직전" 출력 후 3 반환
print("Next 3:", next(gen))  # 출력: 세 번째 yield 직전 / Next 3: 3

# 네 번째 next() 호출: 더 이상 반환할 값이 없으므로 StopIteration 발생
try:
    print("Next 4:", next(gen))
except StopIteration:
    print("더 이상 생성할 값이 없습니다.")

 

   2) 지연 평가의 장점 

      - 제너레이터 표현식을 사용하여 1부터 10까지의 수의 제곱을 계산하는 방법을 보여준다.
      - 여기서는 전체 리스트가 아니라 필요한 순간마다 제곱 값을 계산한다.

 

      - 코드 설명:

         - 제너레이터 표현식 (x**2 for x in range(1, 11))는 리스트 컴프리헨션과 유사하지만, 결과를 한꺼번에 메모리에 저장하지 않고 필요할 때마다 계산하여 반환한다.
         - 예를 들어, 루프가 한 번 순회할 때마다 하나의 값만 계산되므로 메모리 효율성이 매우 좋다. 

# 제너레이터 표현식을 사용하여 lazy하게 1부터 10까지의 제곱을 계산
squares = (x**2 for x in range(1, 11))

# for 루프를 통해 제너레이터를 순회하며 각 제곱 값을 출력합니다.
for square in squares:
    # 이 시점에서 각 x에 대해 x**2가 계산됩니다.
    print(square)

 

   3) 복잡한 데이터 처리와 지연 평가

      - 대용량 데이터 처리에 지연 평가를 활용하는 방법을 보여준다. 만약 전체 데이터를 한 번에 계산하면 불필요한 계산과 메모리 사용이 발생하지만, 제너레이터를 사용하면 필요한 부분만 처리할 수 있다.

 

      - 코드 설명:
         - 함수 even_numbers()는 주어진 데이터 내에서 짝수인 경우에만 값을 yield한다.
         - 데이터 전체를 즉시 평가하지 않고, next() 호출 시점에 조건을 검사하여 필요한 값만 반환하므로 지연 평가의 이점을 보여준다.

def even_numbers(data):
    """
    주어진 데이터에서 짝수만 찾아 지연 평가 방식으로 반환하는 제너레이터 함수.
    """
    for num in data:
        # num이 짝수이면
        if num % 2 == 0:
            print(f"{num}은(는) 짝수이므로 반환됩니다.")
            yield num  # 조건에 맞는 경우에만 yield, 필요할 때마다 계산됨.
        else:
            print(f"{num}은(는) 홀수이므로 건너뜁니다.")

# 대용량 데이터(예: 1부터 20까지)
data = range(1, 21)

# 제너레이터 객체 생성
even_gen = even_numbers(data)

# 필요한 첫 3개의 짝수만 사용
for _ in range(3):
    print("다음 짝수:", next(even_gen))

 

    4) 정리

      - 지연 평가(Lazy Evaluation)는 결과가 필요할 때까지 계산을 미루어 메모리 사용량과 연산 비용을 줄이는 기법이다.
      - 제너레이터 함수는 yield 키워드를 통해 이 기능을 구현하며, 호출할 때마다 내부 상태를 유지하며 순차적으로 값을 생성한다.
      - 이 방식은 대용량 데이터 처리, 무한 시퀀스 생성 등 메모리 효율이 중요한 상황에서 특히 유용하다.
      - 위 코드 예제들을 통해 제너레이터가 어떻게 작동하는지, 각 호출 시 내부 상태가 변화하며 필요한 순간에만 계산이 이루어지는지 확인할 수 있다.

댓글