Application/Python

[Python] 이터레이터와 제너레이터 이해하기 (파이썬 제너레이터 입문)

devsalix 2026. 5. 21. 14:03
728x90

이터레이터와 제너레이터 이해하기 (파이썬 제너레이터 입문)

지난 글 끝에서 "근데 학생이 1000만 명이면 어떡하지?" 하고 슬쩍 흘렸잖아요. 사실 그게 오늘 글의 핵심이에요.

리스트에 1000만 명, 1억 명 데이터를 다 담으려고 하면 어떻게 될까요. 컴퓨터가 갑자기 느려지거나, 심하면 멈춰버리기도 해요. 메모리에 그 많은 걸 한꺼번에 올려두려니 당연한 일이죠.

그래서 파이썬에는 "필요할 때마다 하나씩 꺼내 쓰는" 방법이 따로 있어요. 그게 바로 오늘 이야기할 이터레이터랑 제너레이터예요.

고급 챕터 첫 글이라 살짝 긴장하셨을 수도 있는데, 비유로 풀면 의외로 쉬워요. 천천히 가볼게요.


리스트는 뷔페, 제너레이터는 분식집

리스트는 뷔페라고 생각하시면 돼요. 손님이 오기 전에 음식을 미리 다 차려놓는 거죠. 100가지 메뉴를 다 만들어서 진열대에 깔아두는 거예요. 바로 볼 수 있어서 좋긴 한데, 자리(메모리)를 엄청 많이 차지하잖아요.

제너레이터는 즉석조리 분식집이에요. 주문이 들어와야 떡볶이를 만들기 시작해요. 한 번에 한 접시만 만들면 되니까 부엌이 좁아도 괜찮고요.

이 한 가지 차이가 오늘 글 전체를 관통해요.


이터러블 vs 이터레이터: 책과 책갈피

용어 두 개가 비슷하게 생겨서 처음엔 진짜 헷갈리실 거예요. 저도 한참 같은 거라고 착각했거든요.

  • 이터러블(iterable): for문에 넣을 수 있는 모든 것. 리스트, 튜플, 문자열, 딕셔너리 다 이터러블이에요.
  • 이터레이터(iterator): 이터러블에서 뽑아낸 "지금 어디까지 봤는지 기억하는" 객체예요.

비유하자면 이터러블은 책 자체, 이터레이터는 그 책에 끼워둔 책갈피예요. 책갈피는 앞으로만 움직이고, 마지막 페이지에 닿으면 거기서 끝이에요.


for문이 사실 뒤에서 하는 일

nums = [1, 2, 3]
for n in nums:
    print(n)

 

이렇게 익숙한 코드인데, 파이썬이 내부에서 진짜로 하는 일은 이거예요.

it = iter(nums)            # 책에 책갈피 끼우기
while True:
    try:
        n = next(it)       # 책갈피 다음 페이지로 한 칸
        print(n)
    except StopIteration:  # 책이 끝나면 신호가 옴
        break              # 멈춤

 

for문은 마법이 아니었어요. iter()로 책갈피 만들고, next()로 한 칸씩 넘기고, 다 끝나면 StopIteration 받아서 멈추는 거예요. 이 세트가 python iterator의 전부입니다.


파이썬 yield: 함수를 일시정지하는 마법

여기부터가 진짜 재밌어요. 이터레이터를 직접 만들려면 코드가 좀 번거로운데, 파이썬은 훨씬 쉬운 길을 마련해뒀거든요. 바로 yield 키워드예요.

함수 안에 yield가 한 줄이라도 있으면, 그 함수는 자동으로 제너레이터 함수가 돼요.

return이랑 헷갈리실 텐데, 제일 큰 차이는 이거예요.

return은 함수가 퇴근하는 거고, yield는 잠깐 자리 비우는 거예요.

return을 만나면 함수는 그대로 끝나요. 근데 yield를 만나면 함수가 "잠깐만요" 하고 그 자리에 멈춰 있다가, 다시 부르면 그 자리부터 이어서 실행돼요. 변수 값이고 뭐고 다 그대로 유지된 채로요.


제너레이터 함수 직접 만들어보기

말로만 들으면 감이 안 오니까 직접 만들어볼게요.

def count_up(n):
    i = 1
    while i <= n:
        yield i          # 여기서 멈춤. 다음에 부르면 이 다음부터.
        i += 1

gen = count_up(3)        # 함수 호출인데도 아직 실행 안 됨
print(next(gen))         # 1  ← 첫 yield까지 실행
print(next(gen))         # 2  ← 멈춘 자리부터 다시
print(next(gen))         # 3

 

신기한 점 하나 짚을게요. count_up(3)을 호출했는데도 안에 있는 코드가 바로 돌아가지 않아요. 제너레이터 함수는 호출하는 순간 제너레이터 객체만 만들어줄 뿐이에요.

실제로 코드가 굴러가는 건 next()를 부른 순간부터예요.

물론 실전에서는 next()를 직접 부를 일은 거의 없어요. 그냥 for문에 넣으면 알아서 다 해줘요.

for n in count_up(5):
    print(n)
# 1 2 3 4 5

 


제너레이터 표현식: 괄호만 바꾸면 끝

리스트 컴프리헨션 기억하시죠? 그거랑 거의 똑같이 생긴 친구가 있어요.

list_ver = [x*x for x in range(5)]   # 리스트 → 메모리에 다 만듦
gen_ver  = (x*x for x in range(5))   # 제너레이터 → 필요할 때 하나씩

print(list_ver)  # [0, 1, 4, 9, 16]
print(gen_ver)   # <generator object ...>

 

대괄호 []만 소괄호 ()로 바꿨을 뿐인데 동작이 완전히 달라져요. 참고로 ()는 튜플이 아니에요. 튜플 컴프리헨션 같은 건 없거든요. 소괄호는 제너레이터예요.

sum이나 max 같은 함수에 바로 넣을 땐 괄호도 생략할 수 있어요.

print(sum(x*x for x in range(5)))    # 30

 


진짜 쓸모는 여기서 나와요

작은 예제만 보면 "그래서 뭐가 좋다는 거지?" 싶을 수 있어요. 진짜 위력은 데이터가 클 때 나와요.

# 1억 개의 제곱을 다루기 — 리스트로는 메모리 폭발
# squares = [x*x for x in range(100_000_000)]  ← 이거 절대 하지 마세요

# 제너레이터: 괄호만 바꿨을 뿐인데 메모리 거의 안 씀
squares = (x*x for x in range(100_000_000))
print(sum(squares))   # 한 번에 하나씩만 처리하니까 OK

 

그리고 실무에서 진짜 많이 쓰는 패턴, 대용량 파일을 한 줄씩 읽는 거예요.

def read_big_file(path):
    with open(path, encoding='utf-8') as f:
        for line in f:
            yield line.strip()      # 한 줄 줬다가 멈춤

# 10GB 로그 파일이어도 메모리 부담 거의 없음
for line in read_big_file('huge.log'):
    if 'ERROR' in line:
        print(line)

 

10GB짜리 파일도 메모리 1GB짜리 노트북에서 처리할 수 있어요. 제너레이터 덕분에요.


자주 하는 실수 두 가지

처음에 거의 모든 분이 부딪히는 함정이 있어요.

첫 번째, 제너레이터는 일회용이에요.

gen = (x*x for x in range(5))
print(list(gen))   # [0, 1, 4, 9, 16]
print(list(gen))   # []  ← 어? 왜 비어요?

 

책갈피가 이미 마지막 페이지에 도달했으니까요. 다시 보고 싶으면 책갈피를 새로 끼워야 해요. 즉, 제너레이터를 다시 만들어야 해요.

두 번째, 큰 제너레이터에 list() 씌우면 의미가 없어요.

메모리 아끼려고 제너레이터 만들었는데, list(gen) 하는 순간 1억 개가 한꺼번에 메모리로 올라와요. 도루묵이죠. 굳이 리스트로 안 바꾸고 for문에 바로 넣어 쓰세요.


직접 해보기: 피보나치

오늘 배운 걸 다 녹인 예제예요.

def fibonacci():
    a, b = 0, 1
    while True:          # 무한 루프인데도 괜찮아요
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib), end=' ')
# 0 1 1 2 3 5 8 13 21 34

 

무한 수열인데도 메모리 걱정이 없어요. 필요한 만큼만 꺼내 쓰니까요. 무한 리필 음료라고 생각하시면 돼요.

과제: 위 피보나치를 이용해서 처음 20개의 피보나치 수 중 짝수만 출력해보세요. 힌트는 for문 + if문 조합이에요.


큰 데이터가 들어와도 이제 좀 덜 무서우시죠. 메모리 폭발 걱정 없이 한 줄씩, 하나씩 처리하면 되니까요.

다음 글에서는 좀 결이 다른 얘기를 해볼 거예요. 코드가 길어지고 함수가 많아지면, 이 매개변수가 뭘 받는 건지 헷갈리는 순간이 와요. 그걸 미리 표시해두는 방법이 있는데, 타입 힌팅이라고 해요. 고급은 어려운 게 아니라 코드를 더 단단하게 만드는 단계라는 걸 그때 더 느끼실 거예요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90