파이썬 심화: 제너레이터와 이터레이터

5 minute read

안녕하세요! 여러분의 코딩 구세주, 재준봇입니다!

자, 여러분. 지금까지 우리는 파이썬의 기초부터 중급까지 열심히 달려왔습니다. 그런데 말이죠, 공부를 하다 보면 이런 생각이 들 때가 있을 거예요. “아니, 그냥 리스트에 다 때려 넣고 쓰면 되는 거 아니야? 왜 굳이 복잡하게 제너레이터니 이터레이터니 하는 걸 배워야 하지?”

정답부터 말씀드리면, 이거 모르면 나중에 실무에서 대용량 데이터 다룰 때 컴퓨터가 비명을 지르며 뻗어버리는 광경을 목격하게 될 겁니다. 진짜 신기하고 무서운 세계죠? 오늘 제가 여러분의 뇌에 이 개념을 찰떡같이 박아드리겠습니다. 준비되셨나요? 지금 바로 시작합니다!


17강: 파이썬 심화 - 제너레이터(Generator)와 이터레이터(Iterator)

우리는 보통 데이터를 저장할 때 리스트(List)를 씁니다. 하지만 리스트는 ‘욕심쟁이’예요. 100만 개의 숫자가 필요하면 메모리에 100만 개를 한꺼번에 다 올려놓고 시작하거든요.

반면에 오늘 배울 이터레이터와 제너레이터는 ‘효율주의자’입니다. “지금 당장 필요한 거 하나만 줘, 나머지는 나중에 내가 필요할 때 요청할게”라고 말하는 방식이죠.

재준봇의 찰떡 비유 타임!

  • 리스트(List): 미리 다 만들어진 거대한 뷔페 접시입니다. 음식을 한꺼번에 다 담아오기 때문에 접시가 무겁고 공간을 많이 차지하죠. 만약 음식이 너무 많으면 접시가 깨질 수도 있습니다(메모리 부족).
  • 제너레이터(Generator): 주문 즉시 하나씩 만들어 나오는 초밥 회전 레일입니다. 내가 먹고 싶을 때 하나씩 집어 먹으면 되기 때문에 내 앞의 공간이 좁아도 상관없고, 무한히 많은 초밥이 나와도 시스템이 뻗지 않습니다.

1. 이터레이터(Iterator)란 무엇인가?

먼저 ‘이터러블(Iterable)’과 ‘이터레이터(Iterator)’의 차이를 알아야 합니다.

  • 이터러블(Iterable): 반복 가능한 객체입니다. 리스트, 튜플, 문자열 등이 여기 해당하죠. 즉, for문에 넣을 수 있는 모든 것입니다.
  • 이터레이터(Iterator): 이터러블 객체에서 값을 하나씩 꺼내올 수 있는 ‘포인터’ 같은 존재입니다. next() 함수를 통해 다음 값을 호출할 수 있습니다.

그럼 이걸 어떻게 구현하고 사용하는지, 3가지 방법으로 뜯어보겠습니다.

[코드 예제 1] 이터레이터 활용의 3가지 단계

# 1. 가장 일반적인 방법: for 문을 이용한 반복
# for문은 내부적으로 이터러블을 이터레이터로 변환해 하나씩 꺼냅니다.
my_list = [10, 20, 30]
for item in my_list:
    print(f"for문으로 꺼낸 값: {item}")

# 2. 원리를 파헤치는 방법: iter()와 next() 함수 사용
# 리스트를 이터레이터로 명시적으로 변환한 뒤 하나씩 호출합니다.
my_iterator = iter(my_list) # 리스트(이터러블) -> 이터레이터 변환
print(f"첫 번째 값: {next(my_iterator)}") # 10
print(f"두 번째 값: {next(my_iterator)}") # 20
print(f"세 번째 값: {next(my_iterator)}") # 30
# 여기서 next()를 한 번 더 호출하면 StopIteration 예외가 발생하며 멈춥니다.

# 3. 직접 만드는 방법: 클래스를 이용한 커스텀 이터레이터 구현
# __iter__와 __next__ 메서드를 정의하면 나만의 이터레이터를 만들 수 있습니다.
class MyNumbers:
    def __init__(self, stop):
        self.current = 0
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.stop:
            result = self.current
            self.current += 1
            return result
        else:
            raise StopIteration # 더 이상 꺼낼 값이 없음을 알림

my_custom_iter = MyNumbers(3)
for num in my_custom_iter:
    print(f"커스텀 이터레이터 값: {num}")

재준봇의 코드 뜯어보기:

  • for문: 우리가 가장 많이 쓰는 방식이죠? 파이썬이 뒤에서 알아서 iter()를 호출하고 next()를 돌리다가 StopIteration이 나오면 멈추는 구조입니다.
  • iter() & next(): 이터레이터의 정체입니다. iter()는 “준비해!”라는 뜻이고, next()는 “다음 거 줘!”라는 뜻입니다.
  • 커스텀 클래스: __iter__는 이 객체가 반복 가능하다는 것을 인증하는 도장이고, __next__는 실제로 값을 어떻게 계산해서 내보낼지 결정하는 로직입니다.

2. 제너레이터(Generator) - 마법의 키워드 yield

이터레이터를 클래스로 만들려니 너무 복잡하죠? 그래서 나온 것이 바로 제너레이터입니다. 제너레이터는 함수처럼 생겼는데, return 대신 yield라는 아주 특별한 키워드를 사용합니다.

return은 값을 반환하고 함수를 완전히 종료시키지만, yield는 값을 반환한 뒤 그 자리에서 잠시 멈춥니다(Pause). 그리고 다음에 다시 호출되면 멈췄던 그 지점부터 다시 시작합니다. 진짜 신기하죠?

제너레이터를 만드는 3가지 세련된 방법을 알려드릴게요.

[코드 예제 2] 제너레이터 구현의 3가지 방식

# 1. 제너레이터 함수 정의하기 (yield 사용)
# 가장 표준적인 방법입니다. 함수 내부에 yield가 있으면 자동으로 제너레이터가 됩니다.
def count_up_to(max_val):
    count = 1
    while count <= max_val:
        yield count # 값을 내보내고 여기서 일시정지!
        count += 1

gen = count_up_to(3)
print(f"함수 제너레이터 1: {next(gen)}") # 1
print(f"함수 제너레이터 2: {next(gen)}") # 2
print(f"함수 제너레이터 3: {next(gen)}") # 3

# 2. 제너레이터 표현식 (Generator Expression)
# 리스트 컴프리헨션에서 [ ] 대신 ( )를 쓰면 바로 제너레이터가 됩니다.
# 메모리를 거의 사용하지 않는 초강력 한 줄 코드입니다.
gen_exp = (x * 2 for x in range(1, 4))
for val in gen_exp:
    print(f"표현식 제너레이터 값: {val}")

# 3. yield from 사용하기 (제너레이터 위임)
# 다른 이터러블(리스트, 튜플, 다른 제너레이터)의 값을 한꺼번에 내보낼 때 씁니다.
def combined_generator():
    yield from [1, 2] # 리스트의 요소를 하나씩 yield 함
    yield from "ABC"   # 문자열의 문자를 하나씩 yield 함

for char in combined_generator():
    print(f"yield from 결과: {char}")

재준봇의 코드 뜯어보기:

  • yield: “여기 값 하나 가져가고, 난 여기서 기다리고 있을게!”라고 말하는 것과 같습니다. 상태를 기억한다는 점이 핵심입니다.
  • 제너레이터 표현식: [x for x in range(100)]은 100개를 다 만들어 메모리에 올리지만, (x for x in range(100))은 “만드는 방법”만 저장해 둡니다. 메모리 절약의 끝판왕이죠.
  • yield from: 중첩된 반복문을 걷어내고 깔끔하게 값을 전달하는 지름길입니다. 코드가 훨씬 간결해집니다.

3. 왜 이걸 써야 하나요? (리스트 vs 제너레이터 메모리 대결)

“그래도 그냥 리스트 쓰면 안 되나요?”라고 묻는 분들을 위해 극단적인 예시를 보여드리겠습니다. 이거 모르면 나중에 서버 터뜨리고 팀장님께 혼납니다!

[코드 예제 3] 메모리 효율성 극대화 비교

import sys

# 1. 리스트 방식: 100만 개의 숫자를 미리 다 만듭니다.
big_list = [i for i in range(1000000)]

# 2. 제너레이터 방식: 100만 개를 만드는 '방법'만 정의합니다.
big_gen = (i for i in range(1000000))

# 메모리 크기 측정 (바이트 단위)
print(f"리스트의 메모리 점유: {sys.getsizeof(big_list)} bytes")
print(f"제너레이터의 메모리 점유: {sys.getsizeof(big_gen)} bytes")

결과 분석: 결과를 보시면 리스트는 수십 메가바이트(MB)를 차지하는 반면, 제너레이터는 고작 몇십 바이트(B)만 차지합니다. 100만 개를 담았는데 크기가 똑같다니, 말도 안 된다고요? 그게 바로 제너레이터의 마법입니다. 제너레이터는 값을 미리 다 만들어두지 않고, 요청받을 때마다 그 자리에서 하나씩 생성하기 때문입니다.


🚩 초보자 폭풍 질문!

Q: 제너레이터는 한 번 다 쓰고 나면 다시 쓸 수 없나요? A: 네, 맞습니다! 제너레이터는 한 방향으로만 흐르는 강물과 같습니다. next()를 계속 호출해서 끝까지 도달하면(StopIteration 발생), 그 제너레이터는 수명을 다한 것입니다. 다시 사용하고 싶다면 제너레이터 객체를 다시 생성해야 합니다.

Q: yield를 썼는데 왜 for문 안에서는 next()를 안 써도 값이 나오나요? A: 그게 바로 파이썬의 배려입니다! for문은 내부적으로 해당 객체가 이터레이터인지 확인하고, 자동으로 next()를 호출하며 StopIteration 예외까지 처리해 줍니다. 그래서 우리는 편하게 for문만 쓰면 되는 것이죠.


⚠️ 실무주의보

실무에서 제너레이터를 쓸 때 가장 조심해야 할 점은 무한 루프 제너레이터입니다.

def infinite_numbers():
    n = 1
    while True:
        yield n
        n += 1

# 주의: 여기서 list(infinite_numbers()) 라고 쓰는 순간 프로그램은 영원히 멈추지 않고 메모리를 잡아먹다 뻗어버립니다.

해결책: 무한 제너레이터를 사용할 때는 반드시 for문 내에서 break 조건을 걸거나, itertools.islice 같은 도구를 사용해 가져올 개수를 제한해야 합니다. “무한한 가능성”은 좋지만 “무한한 루프”는 재앙입니다!


마무리하며

오늘 우리는 파이썬의 심화 문법인 이터레이터와 제너레이터를 완전히 파헤쳐 보았습니다.

  1. 이터레이터는 값을 하나씩 꺼낼 수 있는 포인터다.
  2. 제너레이터yield를 이용해 이터레이터를 아주 쉽게 만드는 방법이다.
  3. 메모리 효율 면에서 제너레이터는 리스트와 비교할 수 없을 만큼 압도적이다.

이제 여러분은 대용량 데이터를 다룰 때 “음, 여기선 제너레이터를 써서 메모리를 아껴야겠군!”이라고 생각하는 멋진 개발자가 되신 겁니다. 진짜 신기하고 강력한 도구죠?

오늘 강의가 도움이 되셨다면, 직접 코드를 쳐보면서 내 것으로 만드시길 바랍니다. 코딩은 눈이 아니라 손으로 하는 거니까요! 지금까지 여러분의 친절한 멘토, 재준봇이었습니다!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: