이터레이터(iterator)는 파이썬에서 매우 중요한 역할을 합니다. 이터레이터를 활용하면 반복(iteration)이 가능한 객체를 만들 수 있으며, for in 루프를 통해 데이터에 손쉽게 접근할 수 있습니다.
하지만 이터레이터에는 한 가지 불편한 점이 있습니다. 바로 직접 구현하기가 번거롭다는 것입니다. 유용한 도구임은 분명하지만, 동작하도록 만들려면 상당히 많은 코드를 작성해야 하죠. 바로 이 문제를 해결해 주는 것이 제너레이터(generator)입니다.
이 글에서는 파이썬 제너레이터가 무엇인지, 왜 사용해야 하는지 살펴보고, 실제로 제너레이터를 구현해 보면서 그 동작 원리까지 함께 이해해 보겠습니다.
제너레이터란 무엇일까?
제너레이터는 파이썬에서 이터레이터를 아주 간단하게 만들 수 있는 방법입니다. 함수 형태로 정의되며, 반복 가능한(iterable) 객체를 반환합니다.
제너레이터는 흔히 '문법적 설탕(syntactic sugar)'이라고 불립니다. 새로운 기능을 추가하는 것이 아니라, 기존 기능을 더 효율적이고 깔끔하게 사용할 수 있게 도와주기 때문입니다.
이터레이터와 마찬가지로 제너레이터도 한 번만 순회할 수 있습니다. 제너레이터는 자신이 몇 번 순회되었는지 내부적으로 기억하며, 초기화(reset)할 수 없기 때문입니다.
먼저 일반적인 파이썬 이터레이터의 예시를 살펴보겠습니다.
class Cookies: def __init__(self, value): self.value = value def __iter__(self): return self def __next__(self): return self.value
위 코드는 Cookies라는 이름의 이터레이터를 생성합니다. 데이터를 이 이터레이터에 전달하면 반복 가능한 객체로 변환되어, for in 루프로 데이터를 순회할 수 있습니다. 코드가 꽤 길다고 느껴지시나요? 네, 실제로 길어서 그렇습니다.
제너레이터를 사용하면 이 코드를 훨씬 짧게 줄일 수 있습니다. 위 이터레이터를 제너레이터로 다시 작성해 보겠습니다.
def cookies(value): while True: yield value
코드의 길이가 극적으로 줄어들었습니다. 어떻게 이런 마법이 가능한 걸까요? 그 원리를 살펴보겠습니다.
제너레이터는 return 문 대신 yield 키워드를 사용하는 함수입니다. yield 문은 함수로부터 값을 반환하는 역할을 합니다.
yield 문과 return 문의 결정적인 차이는 다음과 같습니다. return 문은 함수의 실행을 완전히 종료시키지만, yield 문은 함수를 일시 정지(pause)시켰다가 다음 순회 시 중단된 지점부터 이어서 실행합니다.
간단한 제너레이터 함수를 실제로 순회해 보겠습니다.
for cookie in cookies("Raspberry"):
print(cookie)실행 결과:
Raspberry
Raspberry
…
코드가 프로그램을 종료할 때까지 계속 반복됩니다. while 루프가 무한히 실행되도록 작성되었기 때문입니다. 대부분의 경우 이런 무한 출력은 그다지 유용하지 않죠. 이번에는 특정 작업이 끝나면 멈추는 제너레이터를 작성해 보겠습니다.
파이썬 제너레이터 작성하기
콘솔에 출력하고 싶은 쿠키 목록이 있다고 가정해 보겠습니다. 목록은 다음과 같습니다.
["Raspberry", "Choc-Chip", "Cinnamon", "Oat"]
이 목록을 콘솔에 출력하려면 간단한 제너레이터를 만들면 됩니다. 새 파이썬 파일을 열고 아래 코드를 입력해 보세요.
def print_cookies(cookies): length = len(cookies) for cookie in range(0, length): yield cookies[cookie]
이 제너레이터는 지정한 목록의 모든 쿠키를 하나씩 순회하며 각 쿠키를 개별적으로 반환합니다. 아직 제너레이터만으로는 동작하지 않으므로, for 루프를 사용해 순회해야 합니다.
cookie_list = ["Raspberry", "Choc-Chip", "Cinnamon", "Oat"] for c in print_cookies(cookie_list): print(c)
네 가지 쿠키를 저장한 cookie_list 배열을 정의한 뒤, for 루프와 제너레이터를 사용해 목록의 값들을 하나씩 순회했습니다.
for 루프가 반복될 때마다 생성된 객체가 콘솔에 출력됩니다.
Raspberry
Choc-Chip
Cinnamon
Oat
성공입니다! 이 제너레이터는 cookie_list 변수의 모든 값을 순회할 때까지 값을 계속 반환합니다.
제너레이터의 활용 사례는 무궁무진합니다. 예를 들어 주문 금액 목록의 누적 합계를 구하고 싶다면 다음과 같이 작성할 수 있습니다.
def total_orders(orders): length = len(orders) total = 0 for o in range(0, length): total += orders[o] yield total
이 제너레이터는 목록에 있는 모든 주문 금액의 누적 합계를 관리합니다. 각 항목을 순회할 때마다 현재까지의 누적 합계를 yield로 반환합니다. 이 제너레이터를 사용하는 for 루프를 작성해 보겠습니다.
order_list = [2.30, 2.50, 1.95, 6.00, 7.50, 2.15] for order in total_orders(order_list): print(order)
실행 결과:
2.3
4.8
6.75
12.75
20.25
22.4
제너레이터가 order_list 변수에 담긴 모든 주문의 누적 금액을 계산했습니다. 목록을 순회할 때마다 새로운 누적 값이 출력되는 것을 확인할 수 있습니다.
제너레이터 표현식 작성 방법
제너레이터는 이미 이터레이터보다 훨씬 쉽게 작성할 수 있습니다. 그런데 더 깔끔한 코드를 향한 여정은 여기서 끝나지 않습니다. 바로 제너레이터 표현식(generator expression) 덕분입니다.
제너레이터 표현식은 리스트 컴프리헨션(list comprehension)과 비슷합니다. 다만 리스트 컴프리헨션은 전체 리스트를 한 번에 생성하는 반면, 제너레이터 표현식은 제너레이터처럼 항목을 한 번에 하나씩 생성한다는 차이가 있습니다.
앞선 쿠키 예제를 제너레이터 표현식으로 작성해 보겠습니다.
cookie_list = ["Raspberry", "Choc-Chip", "Cinnamon", "Oat"] cookie_generator = (cookie for cookie in cookie_list)
cookie_list 변수에 쿠키 목록을 정의한 후, 제너레이터 표현식을 생성했습니다. 이 표현식은 리스트 컴프리헨션과 문법이 거의 같지만 한 가지 큰 차이가 있습니다. 리스트 컴프리헨션은 대괄호([ ]) 안에 정의하는 반면, 제너레이터 표현식은 소괄호(( )) 안에 정의합니다.
이 표현식을 순회하는 for 루프를 만들어 보겠습니다.
for cookie in cookie_generator: print(cookie)
실행 결과:
Raspberry
Choc-Chip
Cinnamon
Oat
출력 결과는 첫 번째 예제와 동일하지만, 문법이 훨씬 더 명확해졌습니다.
여기서 주의할 점이 있습니다. 제너레이터 표현식으로 선언한 제너레이터는 함수처럼 호출하지 않습니다. 위의 cookie_generator는 별도의 입력값을 받지 않으며, cookie_list를 순회하는 데 필요한 코드를 이미 내장하고 있기 때문입니다.
제너레이터 표현식은 간단한 기능을 수행하는 제너레이터를 작성할 때만 사용하는 것이 좋습니다. 값 목록을 출력하거나, 목록의 값들에 특정 숫자를 곱하는 것이 대표적인 예입니다.
제너레이터 표현식 문법은 명확하지만 기본적으로 한 줄짜리 표현식을 위해 설계되었기 때문입니다. 두 줄 이상의 로직이 필요하다면, 이 글 앞부분에서 설명한 제너레이터 함수 방식으로 작성하는 것이 바람직합니다.
제너레이터를 사용하는 이유
제너레이터가 사용되는 첫 번째 이유는 이터레이터보다 훨씬 간결하기 때문입니다.
제너레이터는 함수처럼 정의되며, __init__, __iter__, __next__ 메서드 없이도 동작합니다. 반면 이터레이터는 이 세 가지 메서드를 모두 구현해야 하죠. 덕분에 제너레이터는 이터레이터보다 훨씬 적은 코드로 구현할 수 있습니다.
두 번째 이유는 메모리 효율성입니다. 제너레이터는 한 번에 하나의 항목만 생성하며, 결과를 반환하기 전에 전체 시퀀스를 메모리에 담아두지 않습니다. 따라서 대용량 데이터를 순회해야 할 때 제너레이터는 매우 실용적인 선택입니다.
이것이 가능한 이유는 제너레이터가 지연 평가(lazy evaluation) 방식을 사용하기 때문입니다. 즉, 다음 요소가 요청되는 시점에 비로소 해당 요소를 생성합니다.
마무리 (그리고 도전 과제)
제너레이터를 활용하면 더 '파이썬답게(Pythonic)' 이터레이터를 만들 수 있습니다.
제너레이터를 사용하면 __init__, __iter__, __next__ 문을 일일이 작성하지 않고도 이터레이터 프로토콜을 따르는 객체를 손쉽게 만들 수 있습니다. 제너레이터는 함수로 정의되며, yield 문을 통해 실행을 잠시 멈추고 값을 메인 프로그램에 전달한 뒤, 다시 원래 작업을 이어갑니다.
실력을 더 키우고 싶다면 아래 도전 과제에 도전해 보세요.
- 문자열을 거꾸로 반환하는 제너레이터 작성하기
- 값 목록에서 특정 단어를 포함하는 값만 반환하는 제너레이터 작성하기
- 리스트의 모든 숫자에 2를 곱해 반환하는 제너레이터 작성하기
더 공부하고 싶다면 파이썬 이터레이터 작성 및 활용 튜토리얼도 함께 확인해 보세요. 이제 여러분도 전문가처럼 파이썬 제너레이터를 자유롭게 활용할 준비가 되었습니다!