파이썬의 제너레이터

파이썬의 제너레이터

이 문서에서는 파이썬의 제너레이터에 대해 설명합니다.

YouTube Video

파이썬의 제너레이터

개요

파이썬의 제너레이터는 반복 처리를 효율적으로 수행할 수 있는 강력한 기능이며, 반복자의 일종입니다. 제너레이터를 사용하면 대용량 데이터 처리 시 메모리 효율적인 코드를 작성할 수 있습니다.

제너레이터란?

파이썬에서 제너레이터는 yield 키워드로 정의되며, 한 번에 하나씩 값을 생성하는 특별한 함수입니다. 실행을 중단하면서 현재 상태를 보존하고, 나중에 다시 실행을 재개할 수 있는 것이 특징입니다.

yield의 기초

yield값을 반환하면서 함수의 실행을 일시 중단시키는 키워드입니다.

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • 이런 함수를 호출하면, 값을 하나씩 반환하는 제너레이터 객체가 생성됩니다.
  • next()를 호출했을 때 다음 값이 없으면 StopIteration 오류가 발생합니다.

next()StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • 이렇게 StopIteration 오류를 명시적으로 처리하면, 제너레이터가 완료되었는지 감지할 수 있습니다.

send(value)

send(value)를 호출하면 제너레이터가 다시 시작되고, valueyield 표현식의 위치로 전달됩니다. 보내진 값은 제너레이터 측에서 yield 표현식의 반환값으로 받을 수 있습니다. 처음 호출할 때는 send(value)None 이외의 값을 보낼 수 없으므로, 반드시 next()send(None)을 사용해야 합니다.

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • send(10)을 사용하면 제너레이터의 yield10을 반환하는 표현식이 되며, 10이 x에 할당됩니다.

throw()

throw를 호출하면 제너레이터가 다시 시작되며, 일시 중단된 yield 위치에서 예외가 발생합니다. 제너레이터 내부에서 예외를 처리하여 처리를 계속할 수 있습니다. 예외가 처리되지 않으면 외부로 전파되어 제너레이터가 종료됩니다.

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • 이 코드에서는 throw를 호출하여 제너레이터에 예외를 주입합니다. 제너레이터 측에서는 예외를 처리하고 recovered를 반환합니다.

close()

close()를 호출하면 제너레이터가 종료됩니다. 제너레이터 내부에서 finally를 사용하여 정리 작업을 수행할 수 있습니다. close() 호출 후 next() 또는 send()를 호출하면 StopIteration 오류가 발생합니다.

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • 이 코드는 close()를 호출하면 제너레이터가 종료되고, finally에서 정리 작업이 실행되는 것을 보여줍니다.

yield from

yield from서브제너레이터에 위임할 때 사용하는 구문입니다. 이것은 제너레이터 내부에서 또 다른 제너레이터를 호출하고, 그 모든 값을 외부 스코프로 전달하는 간단한 방법입니다.

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • 이 코드는 yield from을 사용하여 서브제너레이터의 모든 값을 외부 제너레이터로 위임한 다음, 3을 반환합니다.

이터레이터와의 관계

제너레이터는 내부적으로 __iter__()__next__()를 구현하고 있어 이터레이터의 한 종류입니다. 따라서 for 루프 등 반복 가능한 객체에 사용할 수 있습니다.

for 루프와의 연동

파이썬의 for 루프는 내부적으로 next()를 사용해 값을 자동으로 가져옵니다.

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

이 방식에서는 StopIteration 처리도 자동으로 이루어집니다.

무한 제너레이터 만들기

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

무한 반복이 가능한 제너레이터를 만들 수 있지만, 사용할 때에는 주의가 필요합니다.

제너레이터 표현식

괄호를 사용해 작성하는 제너레이터 표현식을 통해 리스트 내포와 유사한 문법으로 제너레이터를 정의할 수 있습니다.

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

리스트 내포와 달리 모든 요소를 한 번에 메모리로 불러오지 않기 때문에 더 메모리 효율적입니다.

제너레이터에서의 예외 처리

제너레이터 내에서 예외가 발생할 수 있습니다. 이 경우 일반 파이썬 코드와 마찬가지로 try-except를 사용합니다.

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

이 예제에서는 0으로 나눌 때 적절한 예외 처리가 이루어집니다.

제너레이터의 스택 트레이스

제너레이터 내부에서 예외가 발생하면, 제너레이터가 다시 실행될 때 그 예외가 발생합니다.

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • 이 제너레이터는 먼저 1을 반환합니다. 다시 실행할 때 발생한 오류가 포착되어 에러 메시지로 표시됩니다.

제너레이터 활용 예시

대용량 파일을 한 줄씩 읽기

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • 이 함수는 이터레이터로 텍스트 파일을 한 줄씩 읽고, 각 줄의 공백을 제거한 뒤 제너레이터로 반환하여, 대용량 파일을 적은 메모리로 처리할 수 있게 해줍니다.

피보나치 수열 제너레이터

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • 이 코드는 제너레이터를 사용하여 상한값보다 작은 피보나치 수를 순차적으로 생성하고, for 반복문을 통해 출력합니다.

활용 예시

제너레이터는 다음과 같은 상황에서도 사용할 수 있습니다.

  • 대용량 CSV 또는 로그 파일 순차 처리
  • API 페이지네이션 처리
  • 스트리밍 데이터 처리 (예: Kafka, IoT 장치)

요약

개념 주요 포인트
yield 중단하면서 값을 반환
제너레이터 함수 yield를 포함하고 호출 시 이터레이터를 반환하는 함수
장점 메모리 효율적이며 대용량 데이터 처리에 적합함
제너레이터 표현식 (x for x in iterable)과 같은 간결한 문법 사용 가능

제너레이터를 사용하면 메모리를 절약하면서도 대용량 데이터를 효율적으로 처리할 수 있으며, 코드도 간결하게 유지할 수 있습니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video