본문 바로가기
IT/파이썬 기초 완전 정복

파이썬 제너레이터 이해하기 (18)

by 지식 발전소 2024. 4. 20.
728x90
반응형

 

제너레이터란 무엇인가?

 

 

안녕하세요. 이번에는 파이썬 제너레이터(Generator)에 대해 알아보겠습니다. 제너레이터는 프로그래머라면 반드시 숙지해야 할 중요한 개념 중 하나입니다. 그럼 제너레이터가 무엇일까요?

 

제너레이터는 특별한 형태의 함수로서, 함수 실행 흐름을 제어할 수 있는 일종의 이터레이터(Iterator)입니다. 이터레이터란 순회 가능한(iterable) 객체로부터 값을 차례대로 꺼내는 객체를 말합니다. 예를 들어 리스트, 딕셔너리 등도 이터레이터의 일종이죠.

 

그렇다면 제너레이터가 일반 이터레이터와 다른 점은 무엇일까요? 제너레이터는 값을 미리 전부 생성하지 않고, 필요할 때마다 값을 하나씩 생성한다는 특징이 있습니다. 이를 지연 평가(Lazy Evaluation) 라고 합니다.

 

왜 지연 평가가 중요할까요? 대용량 데이터를 처리할 때 데이터 전체를 한꺼번에 메모리에 로드하면 메모리 부족 문제가 발생할 수 있습니다. 하지만 제너레이터를 사용하면 필요한 부분만 메모리에 로드하므로 메모리를 효율적으로 사용할 수 있습니다.

 

그렇다면 제너레이터를 어떻게 만들고 사용할 수 있을까요? 제너레이터에 관한 구체적인 내용을 차근차근 알아보도록 하겠습니다.

 

제너레이터 함수

제너레이터를 만드는 가장 일반적인 방법은 제너레이터 함수를 사용하는 것입니다. 제너레이터 함수는 일반 함수와 거의 동일하지만, 반환값으로 yield 키워드를 사용합니다.

def count_up_to(value):
    n = 0
    while n < value:
        yield n
        n += 1

# 제너레이터 객체 생성
counter = count_up_to(3)

# 제너레이터 객체는 이터레이터입니다.
print(counter)  # <generator object count_up_to at 0x7f14b123ad60>

# 값을 차례대로 생성
print(next(counter))  # 0
print(next(counter))  # 1 
print(next(counter))  # 2
print(next(counter))  # StopIteration 예외 발생

 

위 예시에서 yield 키워드는 함수의 실행을 중단시키고, 그 값을 반환합니다. next()를 호출하면 함수가 다시 이어서 실행되다가 yield를 만나면 또 중단됩니다.

 

이렇게 제너레이터 함수를 호출하면 제너레이터 객체가 생성되는데, 이 객체는 이터레이터이므로 next() 함수로 차례대로 값을 생성할 수 있습니다. 더는 생성할 값이 없으면 StopIteration 예외가 발생합니다.

 

제너레이터 함수는 매우 유용한 특징들을 가지고 있습니다. 먼저 제너레이터 객체는 이터레이터이므로, for문과 리스트 컴프리헨션에서 바로 사용할 수 있습니다.

for i in count_up_to(5):
    print(i)

# 출력 결과
# 0 
# 1
# 2 
# 3
# 4

 

또한 제너레이터 함수는 코루틴(Coroutine) 으로도 활용될 수 있습니다. 코루틴이란 일반 함수와 비슷하지만, 여러 번 호출되어 실행을 개시하고 중단할 수 있는 함수를 말합니다.

def counter():
    n = 0
    while True:
        received = yield n
        if received is None:
            n += 1
        else:
            n = received

count = counter()
next(count)  # 0
next(count)  # 1 
count.send(10)  # 10
next(count)  # 11

 

위 예시에서 yield 앞에 변수를 지정하면 제너레이터에 값을 보낼 수 있습니다. 이렇게 상호작용하는 제너레이터를 코루틴이라고 합니다. 이벤트 루프, 동시성 프로그래밍 등에서 활용됩니다.

제너레이터 표현식

제너레이터를 만드는 또 다른 방법은 제너레이터 표현식입니다. 이는 리스트 컴프리헨션과 문법이 비슷하지만, 대괄호 [] 대신 소괄호 ()를 사용합니다.

squares = (x**2 for x in range(5))

# 제너레이터 이터레이터 객체 생성
print(squares)  # <generator object <genexpr> at 0x7f30680152e0>

# 차례대로 값을 생성
print(next(squares))  # 0 
print(next(squares))  # 1
print(next(squares))  # 4
print(next(squares))  # 9
print(next(squares))  # 16
print(next(squares))  # StopIteration 예외

제너레이터 표현식은 제너레이터 함수보다 간결한 구문을 가지고 있습니다. 하지만 비교적 간단한 연산에만 적합하며, 복잡한 로직에는 제너레이터 함수가 낫습니다.

제너레이터와 메모리 효율성

지연 평가 덕분에 제너레이터는 메모리를 매우 효율적으로 사용할 수 있습니다. 이를 실제 예시로 확인해보겠습니다.

import sys

# 제너레이터 버전
def integer_generator(max):
    for i in range(max):
        yield i

# 리스트 버전 
def integer_list(max):
    return [i for i in range(max)]

print(f"제너레이터 메모리 사용량: {sys.getsizeof(integer_generator(1000000))} bytes")
print(f"리스트 메모리 사용량: {sys.getsizeof(integer_list(1000000))} bytes")

# 출력 결과
# 제너레이터 메모리 사용량: 120 bytes
# 리스트 메모리 사용량: 8697456 bytes

위 예시에서 integer_list()는 1부터 100만까지의 숫자를 모두 리스트에 저장하므로 메모리를 많이 사용합니다. 하지만 integer_generator()는 숫자를 하나씩 생성하므로 메모리 사용량이 현저히 적습니다.

이처럼 대용량 데이터를 처리할 때 제너레이터를 사용하면 메모리 효율성이 크게 향상됩니다. 특히 빅데이터 분석이나 웹 크롤링과 같은 분야에서 제너레이터의 활용도가 높습니다.

제너레이터의 중요 메서드

제너레이터는 이터레이터이므로 이터레이터 프로토콜 메서드를 활용할 수 있습니다. 그리고 제너레이터 고유의 몇 가지 메서드도 있습니다.

1) iter()

제너레이터 객체 자체가 이터레이터이므로, iter() 내장 함수를 호출하면 자기 자신을 반환합니다.

gen = (x**2 for x in range(3))
it = iter(gen)
print(it is gen)  # True

2) next()

앞서 봤듯이 next() 함수로 제너레이터의 다음 값을 생성할 수 있습니다. 값이 없으면 StopIteration 예외가 발생합니다.

3) send()

이 메서드는 제너레이터에 값을 보내고, 다음 출력 값을 받습니다. 코루틴에서 제너레이터와 상호작용할 때 사용합니다.

4) throw()

지정한 예외를 제너레이터 내부로 보냅니다. 제너레이터에서 예외 처리를 하지 않으면 예외가 전파됩니다.

5) close()

제너레이터를 종료시키고 리소스를 정리할 때 사용합니다. 제너레이터가 자동으로 종료되지 않을 때 유용합니다.

이들 메서드를 잘 활용하면 제너레이터를 보다 유연하게 제어할 수 있습니다. 프로그래밍할 때 상황에 맞게 적절히 사용하면 됩니다.

제너레이터를 다루는 핵심 원리

제너레이터의 원리를 이해하면 보다 깊이 있게 활용할 수 있습니다. 제너레이터를 다루는 핵심 원리를 간단히 살펴보겠습니다.

1) 제너레이터는 이터레이터 프로토콜을 따릅니다.

이터레이터 프로토콜에는 __iter__()  __next__() 메서드가 있습니다. 제너레이터 함수에서 yield를 만나면 새 제너레이터 이터레이터가 생성됩니다. 이 이터레이터의 __next__() 메서드가 호출되면 함수 본문이 한 줄씩 실행됩니다.

2) 제너레이터가 생성되면 코드는 실행되지 않습니다.

제너레이터가 생성될 때는 함수 본문의 코드가 아직 실행되지 않습니다. next()를 호출해야 비로소 제너레이터 함수 본문 코드가 실행됩니다.

3) yield로 값을 반환하면 제너레이터는 일시 중지됩니다.

yield 문을 만나면 제너레이터는 그 값을 반환하고 실행을 일시 중지합니다. 다음 next()가 호출되면 제너레이터는 일시 중지 지점부터 다시 실행됩니다.

4) StopIteration으로 제너레이터가 완전히 종료됩니다.

제너레이터 함수 코드가 끝까지 실행되면 StopIteration이 발생하고 제너레이터가 완전히 종료됩니다.

5) send()로 제너레이터와 양방향 통신이 가능합니다.

send() 메서드를 통해 제너레이터에 데이터를 보낼 수 있습니다. 제너레이터 내부에서 yield 앞에 변수를 지정하면 그 변수에 send()로 보낸 값이 저장됩니다.

제너레이터는 파이썬에서 상태를 유지하면서도 효율적으로 메모리를 관리할 수 있는 강력한 기능입니다. 이러한 핵심 원리를 이해한다면 보다 수준 높은 코드를 작성할 수 있을 것입니다.

제너레이터 활용 사례

제너레이터는 다양한 곳에서 활용되고 있습니다. 대표적인 활용 사례를 몇 가지 살펴보겠습니다.

1) 반복 가능한 데이터 스트림 처리

파일 읽기와 같이 큰 데이터를 한 번에 읽을 수 없는 상황에서 제너레이터를 자주 사용합니다. 예를 들어 다음과 같이 대용량 텍스트 파일을 한 줄씩 읽어올 수 있습니다.

def read_large_file(file_path):
    with open(file_path, 'r') as file:
        line = file.readline()
        while line:
            yield line
            line = file.readline()

for line in read_large_file('large_file.txt'):
    print(line)

2) 파이프라인 구현

제너레이터를 사용하면 작업을 여러 단계로 나누고 연결할 수 있습니다. 예를 들어 다음과 같이 간단한 데이터 파이프라인을 구축할 수 있습니다.

def integers():
    for i in range(1, 10):
**2) 파이프라인 구현 **

```python
def integers():
    for i in range(1, 10):
        yield i

def squared(seq):
    for i in seq:
        yield i ** 2
        
def negated(seq):
    for i in seq:
        yield -i

# 파이프라인 구축
negative_squared = negated(squared(integers()))

for ns in negative_squared:
    print(ns)

# 출력 결과
# -1
# -4
# -9
# -16
# -25
# -36
# -49
# -64
# -81

위 예시에서 integers() 제너레이터에서 생성된 숫자들은 squared() 제너레이터로 제곱되고, negated() 제너레이터로 음수화됩니다. 이렇게 제너레이터를 연결하여 파이프라인을 구축할 수 있습니다.

3) 병렬/동시성 프로그래밍

제너레이터를 활용하면 복잡한 병렬 프로그래밍이나 동시성 프로그래밍 작업을 쉽게 처리할 수 있습니다. 파이썬 내장 라이브러리인 itertools와 제너레이터를 함께 사용하면 됩니다.

import math
import itertools

# itertools.count()와 itertools.islice()로 무한 시퀀스를 생성하고 슬라이싱
prime_iter = itertools.islice(
    (x for x in itertools.count(2) if all(x % div for div in range(2, int(math.sqrt(x)) + 1))), 50)

# 제너레이터 출력
for p in prime_iter:
    print(p)

위 예시는 첫 50개의 소수를 계산하여 출력합니다. itertools 모듈의 여러 제너레이터와 조합하여 복잡한 반복 작업을 간단히 구현할 수 있습니다.

4) 이벤트 드리븐 프로그래밍

제너레이터는 코루틴으로 구현될 수 있어 이벤트 드리븐 프로그래밍에 적합합니다. 시간이 지날수록 발생하는 이벤트를 효율적으로 처리하고 관리할 수 있습니다.

import time

def monitor(seconds):
    started = time.time()
    while True:
        elapsed = time.time() - started
        if elapsed >= seconds:
            return elapsed
        event = yield  # 이벤트 대기
        print(f"Got {event}")

mon = monitor(5)
next(mon)  # 제너레이터 실행 시작

mon.send("First")
mon.send("Second")
try:
    elapsed = mon.send("Third")
except StopIteration as e:
    print(f"Monitor stopped after {e.value:.2f} seconds")

위 예시에서 monitor 제너레이터는 특정 시간이 경과하면 종료됩니다. 그 사이에 send()로 이벤트를 보내면 그 이벤트를 처리합니다. 이처럼 제너레이터를 이용하면 시간에 따른 이벤트 처리 로직을 간단히 구현할 수 있습니다.

이렇게 제너레이터는 다양한 분야에서 활발히 활용되고 있습니다. 제너레이터의 지연 평가와 메모리 효율성, 상태 유지 기능 등의 특성 덕분입니다. 상황에 맞게 적절히 활용한다면 코드 가독성과 성능을 크게 향상시킬 수 있습니다.

제너레이터와 성능

앞서 설명한 대로 제너레이터는 지연 평가와 메모리 효율성 측면에서 장점이 있습니다. 하지만 때로는 성능 저하가 발생할 수도 있습니다. 이는 제너레이터의 동작 원리 때문인데요, 제너레이터를 잘못 사용하면 성능 이슈로 이어질 수 있습니다. 그래서 제너레이터를 사용할 때는 다음 사항들을 유의해야 합니다.

1) 제너레이터 사용 목적 숙지

제너레이터는 메모리를 절약하는 것이 주 목적입니다. 따라서 대용량 데이터를 다룰 때 유용합니다. 하지만 작은 데이터셋이라면 오히려 제너레이터를 사용하는 것이 비효율적일 수 있습니다.

2) 제너레이터 논리 복잡도 주의

제너레이터 내부에 복잡한 로직이나 연산이 들어가면 오버헤드가 발생할 수 있습니다. 제너레이터는 단순한 작업에 적합하므로, 복잡한 로직이 필요하다면 다른 방식을 고려해보는 것이 좋습니다.

3) 제너레이터 중첩 주의

제너레이터를 중첩해서 사용하면 성능 저하가 발생할 수 있습니다. 특히 중첩 수준이 깊어질수록 더욱 주의해야 합니다. 가급적 중첩을 피하거나 수준을 낮추는 것이 좋습니다.

4) 제너레이터 반복 횟수 제한

제너레이터를 반복할 때마다 매번 새로운 이터레이터 객체가 생성됩니다. 따라서 제너레이터를 여러 번 반복해야 한다면 성능 저하가 발생할 수 있습니다. 이럴 때는 제너레이터의 결과를 리스트나 다른 자료형으로 변환하는 것이 나을 수 있습니다.

5) 제너레이터 안전성 유의

제너레이터는 상태를 유지하므로 여러 스레드에서 동시에 접근하면 안전성 이슈가 발생할 수 있습니다. 멀티스레딩 환경에서 제너레이터를 사용할 때는 주의가 필요합니다.

6) 대안 고려

상황에 따라 제너레이터 대신 다른 방식을 사용하는 것이 더 나을 수 있습니다. 예를 들어 파이썬의 itertools 모듈이나 yield from 구문을 활용하는 것도 한 방법입니다.

이렇게 제너레이터를 적절히 활용하면 성능 이슈를 최소화할 수 있습니다. 제너레이터의 장단점을 잘 이해하고, 상황에 맞게 적절한 방식을 선택하는 것이 중요합니다.

정리

지금까지 파이썬 제너레이터에 대해 자세히 알아보았습니다. 주요 내용을 요약하면 다음과 같습니다.

  • 제너레이터는 지연 평가로 동작하는 특별한 이터레이터입니다.
  • yield 키워드를 사용하여 제너레이터 함수나 제너레이터 표현식으로 만듭니다.
  • 제너레이터는 메모리 효율성이 뛰어나 대용량 데이터 처리에 유용합니다.
  • next(), send(), throw(), close() 등의 메서드로 제너레이터를 제어할 수 있습니다.
  • 제너레이터는 이터레이터 프로토콜과 코루틴 개념을 바탕으로 동작합니다.
  • 데이터 스트림 처리, 파이프라인 구축, 병렬/동시성 프로그래밍, 이벤트 드리븐 프로그래밍 등에 활용됩니다.
  • 제너레이터는 상황에 따라 성능 이슈가 발생할 수 있으므로, 적절한 사용이 필요합니다.

제너레이터는 함수 실행 흐름을 제어할 수 있는 강력한 기능입니다. 상태를 유지하면서도 메모리를 효율적으로 관리할 수 있죠. 다양한 분야에서 널리 활용되고 있으며, 고급 프로그래밍 기법으로 자리잡고 있습니다. 파이썬을 잘 활용하기 위해서는 제너레이터에 대한 이해가 필수적입니다.

 

부족한 부분이나 더 궁금한 점이 있다면 언제든 질문해주시기 바랍니다.

참고 자료

[1] Python 공식 문서, "제너레이터"

[2] Luciano Ramalho, "Fluent Python"

[3] Brett Slatkin, "Effective Python"

[4] Doug Hellman, "Python 3 Module of the Week"

[5] 김광용, "모두를 위한 파이썬"

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글