Python 이터레이터와 제너레이터

배추·2026년 1월 1일

🐍 Python

목록 보기
22/23

Python의 이터레이터와 제너레이터에 대해 알아봅니다.


  • 반복 가능(iterable) 객체: for 문과 같은 반복 구문에 적용할 수 있는 리스트와 같은 객체.

▶︎이터레이터

  • 이터레이터: 데이터를 하나씩 순서대로 꺼내올 수 있는 객체.
    • next() 함수를 사용하여 값을 하나씩 가져올 수 있다.
    • 모든 값을 다 가져오면 StopIteration 예외가 발생한다.
  • 반복 가능하다고 해서 이터레이터는 아니다.
a = [1, 2, 3]
next(a) # 오류
  • 하지만 반복 가능하다면 다음과 같이 iter 함수를 이용해 이터레이터로 만들 수 있다.
a = [1, 2, 3]
ia = iter(a)
type(ia) # <class 'list_iterator'>

next(ia) # 1
next(ia) # 2
next(ia) # 3
next(ia) # StopIteration 예외
  • next 함수를 호출할 때마다 이터레이터 객체의 요소를 차례대로 반환한다. 하지만 더는 반환할 값이 없다면 StopIteration 예외가 발생한다.
  • 이터레이터의 값을 가져오는 가장 일반적인 방법은 for 문을 이용.
    • for 문을 이용하면 자동으로 값을 가져오므로 next 함수를 직접 호출하거나 StopIteration 예외를 처리할 필요가 없다.
    • 이터레이터는 for 문을 이용하여 반복하고 난 후에는 다시 반복하더라도 더는 그 값을 가져오지 못한다. 즉, for문이나 next로 그 값을 한 번 읽으면 그 값을 다시는 읽을 수 없다.
a = [1, 2, 3]
ia = iter(a)
for i in ia:
    print(i)

# 1
# 2
# 3

이터레이터 만들기

이터레이터 클래스를 만드는 핵심 규칙

  • __iter__ 메서드: 이터레이터 객체 자신을 반환.
  • __next__ 메서드: 다음 값을 반환하고, 더 이상 값이 없으면 StopIteration 예외를 발생.
  • 이 두 메서드만 구현하면 for 문이나 next() 함수에서 사용할 수 있는 이터레이터가 된다.

MyIterator 클래스의 동작 원리

# iterator.py
class MyIterator:
    def __init__(self, data):
        self.data = data
        self.position = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.position >= len(self.data):
            raise StopIteration
        result = self.data[self.position]
        self.position += 1
        return result

if __name__ == "__main__":
    i = MyIterator([1,2,3])
    for item in i:
        print(item)
    1. __init__ 메서드: 이터레이터를 초기화.
    • self.data: 반복할 데이터를 저장.
    • self.position: 현재 위치를 추적하는 변수 (0부터 시작).
    1. __iter__ 메서드: 이터레이터 객체 자신을 반환한다
    • 이 메서드가 있어야 파이썬이 해당 객체를 반복 가능한 객체로 인식한다.
    • for 문, iter() 함수, next() 함수 등에서 사용하기 위해 필수.
    • 보통 return self로 자기 자신을 반환한다.
    1. __next__ 메서드: 다음 값을 반환.
  • self.position을 이용해 현재 위치의 값을 가져온다.
  • 위치를 하나씩 증가시킨다.
  • 더 이상 값이 없으면 StopIteration 예외를 발생시킨다.

=> 이렇게 구현하면 next() 함수나 for 문에서 사용할 수 있는 이터레이터 완성!

입력받은 데이터를 역순으로 출력

# reviterator.py
class ReverseIterator:
    def __init__(self, data):
        self.data = data
        self.position = len(self.data) -1

    def __iter__(self):
        return self

    def __next__(self):
        if self.position < 0:
            raise StopIteration
        result = self.data[self.position]
        self.position -= 1
        return result

if __name__ == "__main__":
    i = ReverseIterator([1,2,3])
    for item in i:
        print(item)

▶︎제너레이터

  • 제너레이터: 간단하게 이터레이터를 만드는 특별한 함수.
  • 이터레이터를 클래스로 만들려면 __iter____next__ 메서드를 구현해야 하는 등 복잡하다. 하지만 제너레이터를 사용하면 함수 하나만으로 간단하게 이터레이터를 만들 수 있다.

제너레이터의 핵심 특징

  • 일반 함수와 비슷하지만 return 대신 yield 키워드를 사용한다.
  • yield를 만나면 값을 반환하고 함수 실행을 일시정지한다.
  • 다시 호출하면 일시정지했던 지점부터 계속 실행한다.
  • 마치 음악 플레이어의 재생/일시정지 기능처럼 동작.

제너레이터 만들기

def mygen():
    yield 'a'
    yield 'b'
    yield 'c'

g = mygen()
type(g) # <class 'generator'>

next(g) # 'a'
next(g) # 'b'
next(g) # 'c'
next(g) # StopIteration 오류
  • mygen 함수는 yield 구문을 포함하므로 제너레이터이다.
  • 제너레이터 객체는 g = mygen()과 같이 제너레이터 함수를 호출하여 만들 수 있다.
  • type 명령어로 확인하면 g 객체는 제너레이터 타입의 객체.
  • 제너레이터 객체 g로 next 함수를 실행하면 mygen 함수의 첫 번째 yield 문에 따라 'a' 값을 반환한다.
  • 제너레이터는 yield라는 문장을 만나면 그 값을 반환하되 현재 상태를 그대로 기억한다.
  • 다시 next() 함수를 실행 시 두 번째 yield 문에 따라 'b' 값을 반환한다.
  • 계속해서 next 함수를 호출하면 StopIteration 오류.

제너레이터 표현식

  • mygen 함수는 1부터 1,000까지 각각의 숫자를 제곱한 값을 순서대로 반환하는 제너레이터.
# generator.py
def mygen():
    for i in range(1, 1000):
        result = i * i
        yield result

gen = mygen()

print(next(gen)) # 1
print(next(gen)) # 4
print(next(gen)) # 9
  • 제너레이터 표현식(generator expression): 제너레이터는 def를 이용한 함수로 만들 수 있지만, 튜플 표현식으로 좀 더 간단하게 만들 수도 있다.
gen = (i * i for i in range(1, 1000))
  • 이 표현식은 mygen 함수로 만든 제너레이터와 완전히 똑같이 기능한다.
  • 여기서 사용한 표현식은 리스트 컴프리헨션(list comprehension) 구문과 비슷. 다만 리스트 대신 튜플을 이용한 점이 다르다.

제너레이터와 이터레이터

  • 제너레이터는 이터레이터와 서로 상당히 비슷.
  • 클래스를 이용해 이터레이터를 작성하면 좀 더 복잡한 행동을 구현할 수 있다.
  • 이와 달리 제너레이터를 이용하면 간단하게 이터레이터를 만들 수 있다.
    • 간단한 경우라면 제너레이터 함수나 제너레이터 표현식을 사용하는 것이 가독성이나 유지 보수 측면에서 유리.
  • => 이터레이터의 성격에 따라 클래스로 만들 것인지, 제너레이터로 만들 것인지를 선택해야 한다.

제너레이터 활용하기

  • 상황: 시간이 오래 걸리는 작업을 처리해야 하는데, 모든 결과가 필요하지 않은 경우.

일반적인 리스트 컴프리헨션

import time

def longtime_job():
    print("job start")
    time.sleep(1)  # 1초 지연 - 실제로는 데이터베이스 조회, 파일 처리 등을 시뮬레이션
    return "done"

# 리스트 컴프리헨션: 5번의 작업을 모두 실행해서 리스트로 만든다
list_job = [longtime_job() for i in range(5)]
print(list_job[0])  # 첫 번째 결과만 필요한 상황
  • 문제점
    • longtime_job() 함수는 1초씩 걸린다.
    • 리스트를 만들 때 5번의 함수를 모두 미리 실행한다.
    • 첫 번째 결과만 필요한데도 5초를 기다려야 한다.

제너레이터를 사용한 해결책

import time

def longtime_job():
    print("job start")
    time.sleep(1)
    return "done"

# 제너레이터 표현식: 함수를 미리 실행하지 않고 필요할 때만 실행
list_job = (longtime_job() for i in range(5))
print(next(list_job))  # 첫 번째 값만 요청
  • 핵심 차이점
    • [ ] (리스트 컴프리헨션) → ( ) (제너레이터 표현식)으로 변경
    • list_job[0] → next(list_job)으로 값을 가져오는 방식 변경.
  • 제너레이터의 동작 방식
    • (longtime_job() for i in range(5))는 함수를 즉시 실행하지 않는다.
    • next(list_job)을 호출할 때 비로소 첫 번째 longtime_job()만 실행한다.
    • 나머지 4개는 실행되지 않는다.
  • 결과 분석: 제너레이터를 사용함으로써 놀라운 성능 개선.
    • 실행 시간이 5초에서 1초로 대폭 단축되었다. 이는 리스트 컴프리헨션에서는 5개의 함수를 모두 실행했지만, 제너레이터에서는 필요한 첫 번째 함수만 실행했기 때문.
    • "job start"가 5번 출력되던 것이 1번만 출력되는 것을 보면, 실제로 함수 호출 횟수가 줄어든 것을 확인할 수 있다.
    • 이와 함께 메모리 사용량도 크게 절약되는데, 리스트는 모든 결과를 메모리에 저장하지만 제너레이터는 필요할 때만 값을 생성하기 때문.
  • 이런 방식을 '느긋한 계산법(lazy evaluation)'이라고 한다.

제너레이터는 다음과 같은 상황에서 매우 유용하다

  • 대용량 데이터 처리 시 메모리 절약
  • 시간이 오래 걸리는 작업을 필요할 때만 실행
  • 무한한 데이터 스트림 처리
profile
난 🥬

0개의 댓글