컴프리헨션 / 제너레이터 정리 / 지연평가
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 키워드를 통해 이 기능을 구현하며, 호출할 때마다 내부 상태를 유지하며 순차적으로 값을 생성한다.
- 이 방식은 대용량 데이터 처리, 무한 시퀀스 생성 등 메모리 효율이 중요한 상황에서 특히 유용하다.
- 위 코드 예제들을 통해 제너레이터가 어떻게 작동하는지, 각 호출 시 내부 상태가 변화하며 필요한 순간에만 계산이 이루어지는지 확인할 수 있다.
'개발언어 Back-End > Python' 카테고리의 다른 글
Python Logging 설정 파일 매뉴얼 (0) | 2025.02.13 |
---|---|
Python Logging 사용 방법 정리 (0) | 2025.02.13 |
파이썬 기반 docker 라이브러리 정리 (0) | 2025.02.09 |
파이썬에서 CPU와 메모리 사용량을 추적하는 방법 (0) | 2024.05.14 |
함수안에 데이터클래스 정의의 용도와 사용 예시 (0) | 2024.03.17 |
댓글