본문 바로가기
프로그래밍/파이썬

파이썬 - 제너레이터

by ennak 2024. 11. 28.
반응형

제너레이터의 정의와 기본 개념

제너레이터는 파이썬에서 이터레이터(iterator)를 생성하는 함수다.
일반 함수와 달리 yield 문을 사용하여 데이터를 하나씩 반환한다.
이는 모든 결과를 메모리에 저장하지 않고, 필요할 때마다 값을 생성할 수 있게 해준다.
제너레이터 함수가 호출되면, 함수 본문이 즉시 실행되지 않는다. 대신, 제너레이터 객체가 반환된다.
이 객체의 next() 메서드가 호출될 때마다 함수는 다음 yield 문까지 실행되고, 해당 값을 반환한다.


  • 간단한 제너레이터 함수의 예시
def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()
print(next(gen))  # 출력: 1
print(next(gen))  # 출력: 2
print(next(gen))  # 출력: 3

이 예시에서 simple_generator 함수는 세 개의 값을 순차적으로 생성한다. next() 함수를 호출할 때마다 다음 값이 반환된다.


제너레이터의 장점

1. 메모리 효율성

제너레이터는 모든 값을 한 번에 메모리에 저장하지 않고, 필요할 때마다 값을 생성한다. 이는 대용량 데이터셋을 다룰 때 특히 유용하다.

2. 성능 향상

필요한 값만 생성하므로, 전체 시퀀스를 미리 계산하는 것보다 초기 응답 시간이 빠르다.

3. 무한 시퀀스 표현

제너레이터를 사용하면 이론적으로 무한한 데이터 스트림을 표현할 수 있다.

4. 코드 간결성

복잡한 이터레이터 로직을 간단한 함수로 표현할 수 있다.


제너레이터 표현식

제너레이터 함수 외에도, 파이썬은 제너레이터 표현식을 지원한다. 이는 리스트 컴프리헨션과 유사하지만 대괄호 대신 괄호를 사용한다.

# 리스트 컴프리헨션
squares_list = [x**2 for x in range(10)]

# 제너레이터 표현식
squares_gen = (x**2 for x in range(10))

실제 사용 사례

1. 대용량 파일 처리

def read_large_file(file_path):
    with open(file_path, 'r') as file:
        for line in file:
            yield line.strip()

for line in read_large_file('huge_file.txt'):
    process_line(line)

이 예시에서 read_large_file 함수는 대용량 파일을 한 줄씩 읽어 처리한다. 전체 파일을 메모리에 로드하지 않고도 효율적으로 처리할 수 있다.


2. 피보나치 수열 생성

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

fib = fibonacci()
for _ in range(10):
    print(next(fib))

이 제너레이터는 무한한 피보나치 수열을 생성한다. 필요한 만큼만 값을 생성할 수 있어 효율적이다.


3. 데이터 파이프라인 구축

def data_source():
    for i in range(100):
        yield i

def process_data(data):
    for item in data:
        yield item * 2

def filter_data(data):
    for item in data:
        if item % 4 == 0:
            yield item

pipeline = filter_data(process_data(data_source()))
for item in pipeline:
    print(item)

이 예시는 데이터 소스, 처리, 필터링의 파이프라인을 제너레이터를 사용해 구현한다. 각 단계가 필요할 때만 실행되어 메모리 사용을 최소화한다.


제너레이터의 고급 기능

1. send() 메서드

제너레이터는 send() 메서드를 통해 외부에서 값을 주입받을 수 있다.

def echo_generator():
    while True:
        received = yield
        yield f"Echo: {received}"

gen = echo_generator()
next(gen)  # 제너레이터 초기화
print(gen.send("Hello"))  # 출력: Echo: Hello
print(gen.send("World"))  # 출력: Echo: World

2. throw() 메서드

제너레이터 내부로 예외를 전달할 수 있다.

def number_generator():
    try:
        yield 1
        yield 2
        yield 3
    except ValueError:
        yield 'Error occurred'

gen = number_generator()
print(next(gen))  # 출력: 1
print(gen.throw(ValueError))  # 출력: Error occurred

3. close() 메서드

제너레이터를 강제로 종료할 수 있다.

def countdown():
    i = 5
    while i > 0:
        yield i
        i -= 1

gen = countdown()
print(next(gen))  # 출력: 5
print(next(gen))  # 출력: 4
gen.close()
print(next(gen))  # StopIteration 예외 발생

제너레이터와 코루틴

제너레이터는 코루틴의 기반이 된다. 코루틴은 여러 진입점을 가진 서브루틴으로, 비동기 프로그래밍에서 중요한 역할을 한다.

import asyncio

async def async_generator():
    for i in range(3):
        await asyncio.sleep(1)
        yield i

async def main():
    async for item in async_generator():
        print(item)

asyncio.run(main())

이 예시는 비동기 제너레이터를 사용하여 비동기적으로 값을 생성하고 처리한다.


제너레이터의 성능 고려사항

제너레이터는 많은 경우에 리스트나 다른 시퀀스 타입보다 효율적이지만, 모든 상황에서 최선의 선택은 아니다.

1. 반복 횟수

데이터를 여러 번 반복해야 하는 경우, 제너레이터는 매번 값을 재생성해야 하므로 비효율적일 수 있다.

2. 인덱싱과 슬라이싱

제너레이터는 인덱싱과 슬라이싱을 직접 지원하지 않는다. 필요한 경우 리스트로 변환해야 한다.

3. 메모리 사용

대용량 데이터를 다룰 때 제너레이터가 유리하지만, 작은 데이터셋에서는 리스트가 더 빠를 수 있다.

  • 성능 비교 예시
import time

def list_approach(n):
    return [i**2 for i in range(n)]

def generator_approach(n):
    return (i**2 for i in range(n))

n = 10**6

# 리스트 방식
start = time.time()
squares_list = list_approach(n)
sum(squares_list)
print(f"List time: {time.time() - start}")

# 제너레이터 방식
start = time.time()
squares_gen = generator_approach(n)
sum(squares_gen)
print(f"Generator time: {time.time() - start}")

이 예시는 큰 수의 제곱을 계산하고 합산하는 데 있어 리스트와 제너레이터의 성능을 비교한다. 대부분의 경우 제너레이터가 더 빠르고 메모리 효율적이다.


제너레이터의 디버깅과 테스팅

제너레이터는 지연 평가(lazy evaluation) 특성 때문에 디버깅이 까다로울 수 있다. 이 때 몇 가지 유용한 팁들이다.

1. 로깅 사용

yield 문 주변에 로그를 추가하여 실행 흐름을 추적한다.

2. 리스트로 변환

디버깅 중에 제너레이터를 리스트로 변환하여 모든 값을 한 번에 확인한다.

3. 단위 테스트 작성

제너레이터 함수의 예상 출력을 검증하는 테스트를 작성한다.

  • 테스트 예시
import unittest

def even_numbers(n):
    for i in range(n):
        if i % 2 == 0:
            yield i

class TestEvenNumbers(unittest.TestCase):
    def test_even_numbers(self):
        result = list(even_numbers(10))
        self.assertEqual(result, [0, 2, 4, 6, 8])

if __name__ == '__main__':
    unittest.main()
반응형

'프로그래밍 > 파이썬' 카테고리의 다른 글

파이썬 - Set  (0) 2024.11.29
파이썬 - 딕셔너리  (0) 2024.11.29
파이썬 - 컴프리헨션  (0) 2024.11.27
파이썬 - 튜플 개념 정리  (0) 2024.11.26
파이썬 - 리스트 메서드 정리  (0) 2024.11.25