14. 이터레이터와 제너레이터
for가 내부에서 어떻게 도는지(iter/next)를 이해한다.- 이터레이터와 이터러블의 차이를 안다.
yield로 제너레이터 함수를 만든다.- 제너레이터가 왜 메모리에 효율적인지(지연 평가) 이해한다.
먼저: 이터레이터가 뭔가요?
초급편 8장에서 for로 리스트·문자열·딕셔너리를 "하나씩" 훑었습니다. 그런데 for는 내부적으로 어떻게 "하나씩" 꺼낼까요?
비밀은 두 내장 함수 iter()와 next()에 있습니다.
- 이터러블(iterable):
for로 돌 수 있는 것. 리스트·문자열·딕셔너리·range 등. - 이터레이터(iterator): 이터러블에서 값을 하나씩 꺼내주는 장치.
next()를 부를 때마다 다음 값을 내놓습니다.
nums = [10, 20, 30] it = iter(nums) # 리스트에서 이터레이터를 얻음 print(next(it)) # 10 (첫 값) print(next(it)) # 20 (다음 값) print(next(it)) # 30 (다음 값) print(next(it)) # ❌ StopIteration! 더 없음
값이 다 떨어지면 StopIteration 예외가 납니다. 이것이 "끝"의 신호입니다.
for는 마법이 아니다. 내부에서 iter()로 이터레이터를 얻고, next()를 반복 호출하다가 StopIteration이 나면 멈춘다.실제로 for가 하는 일을 손으로 풀어 쓰면 이렇습니다.
# for ch in "abc": print(ch) 와 동일한 동작 it = iter("abc") while True: try: ch = next(it) print(ch, end=" ") except StopIteration: break # a b c
flowchart TD
Start(["for x in 이터러블"]):::user --> Iter["iter() 로<br/>이터레이터 획득"]:::proc
Iter --> Next{"next() 호출"}:::proc
Next -->|값 있음| Body["x에 값 담아<br/>블록 실행"]:::result
Body --> Next
Next -->|StopIteration| End(["반복 종료"]):::data
classDef user fill:#fff3b0,stroke:#e0a800,color:#5c4500
classDef proc fill:#7fd8d8,stroke:#2a9d8f,color:#14532d
classDef result fill:#b8e6c1,stroke:#34a853,color:#14532d
classDef data fill:#a8dadc,stroke:#457b9d,color:#1d3557
이걸 알아두면 좋은 이유는, 다음에 배울 제너레이터가 바로 이 이터레이터를 쉽게 만드는 도구이기 때문입니다.
⭐ 제너레이터: yield로 값을 하나씩 내놓기
이터레이터를 직접 만들려면 클래스에 __iter__·__next__를 구현해야 해서 번거롭습니다. 제너레이터(generator)는 이를 함수처럼 간단히 만드는 방법입니다.
핵심은 return 대신 yield를 쓰는 것입니다. yield가 들어간 함수는 자동으로 제너레이터가 됩니다.
def count_up(n): for i in range(1, n + 1): yield i # return이 아니라 yield! gen = count_up(3) print(type(gen)) # <class 'generator'> print(list(gen)) # [1, 2, 3]
return과 yield의 결정적 차이는 이렇습니다.
return: 값을 돌려주고 함수를 완전히 끝낸다. 한 번만 돌려준다.yield: 값을 내놓고 함수를 잠시 멈춘다(일시정지). 다음next()호출 때 멈춘 자리에서 이어서 실행한다.
이 "일시정지" 동작을 직접 보면 이해가 확실해집니다.
def greetings(): print("[함수 시작]") yield "안녕" print("[중간 지점]") yield "반가워" print("[함수 끝]") g = greetings() print(next(g)) # [함수 시작] → 안녕 (첫 yield에서 멈춤) print(next(g)) # [중간 지점] → 반가워 (멈췄던 자리부터 이어 실행)
next(g)를 부를 때마다 함수가 다음 yield까지 실행되고 거기서 멈춥니다. 일반 함수가 한 번에 끝까지 달리는 것과 완전히 다릅니다.
실행 흐름을 한 단계씩 추적하기
yield가 어떻게 "멈췄다 이어지는지" 번호를 매겨 추적해 봅시다. 제너레이터를 비디오 재생에 비유하면, next()는 "다음 yield까지 재생하고 일시정지" 버튼입니다.
def steps(): print(" [A] 함수 시작") yield 1 print(" [B] 첫 yield 다음") yield 2 print(" [C] 둘째 yield 다음") g = steps() # ① 아직 아무것도 실행 안 됨! print("② next →"); v = next(g) # ② [A] 출력 후 yield 1 에서 멈춤, v=1 print("③ next →"); v = next(g) # ③ [B] 출력 후 yield 2 에서 멈춤, v=2 print("④ next →") # ④ [C] 출력 후 함수 끝 → StopIteration
실행하면 이런 순서로 출력됩니다.
① (제너레이터 생성, 출력 없음) ② next → [A] 함수 시작 ← 첫 next에서야 [A]가 실행됨! ③ next → [B] 첫 yield 다음 ← 멈췄던 yield 1 자리부터 이어서 [B] ④ next → [C] 둘째 yield 다음 ← 다시 yield 2 자리부터 이어서 [C], 그 후 StopIteration
여기서 두 가지를 꼭 짚으세요.
- 생성 시점엔 함수 본문이 한 줄도 안 돕니다.
g = steps()는 제너레이터 객체만 만들 뿐,[A]조차 출력하지 않습니다. 첫next()가 와야 비로소 실행이 시작됩니다. - 함수가 자기 위치를 기억합니다. 매
next()마다 "멈췄던yield다음 줄"부터 이어 달립니다. 변수 값도 그대로 유지된 채로요. 마치 책갈피를 끼워둔 책처럼, 다음에 펼치면 그 자리부터 읽습니다.
yield는 "값을 내놓고 그 자리에서 멈췄다가, 다음 호출 때 이어서 달리는" 키워드다. 제너레이터는 자기가 어디까지 실행했는지와 그때의 변수 값을 기억한다. 이 덕분에 값을 하나씩, 필요할 때 만들어낼 수 있다.왜 제너레이터를 쓸까: 지연 평가와 메모리
제너레이터의 가장 큰 장점은 값을 미리 다 만들지 않고, 요청받을 때 하나씩 만든다는 것입니다. 이를 지연 평가(lazy evaluation)라고 합니다.
리스트는 모든 값을 즉시 메모리에 올립니다. 100만 개면 100만 개분의 메모리를 씁니다. 제너레이터는 한 번에 하나만 메모리에 두므로, 아무리 큰 범위라도 메모리를 거의 안 씁니다.
def squares(n): for i in range(n): yield i ** 2 # 1억 개라도 메모리 폭발 없이 합산 가능 # (값을 하나 만들어 더하고 버리고, 다음 값을 만들어 더하고...) total = sum(squares(100)) print(total) # 328350
리스트 [...] | 제너레이터 yield / (...) | |
|---|---|---|
| 값 생성 시점 | 즉시 전부 | 요청할 때 하나씩 |
| 메모리 | 전체를 담음 | 한 번에 하나 |
| 재사용 | 여러 번 순회 가능 | 한 번 쓰면 소진 |
| 적합한 경우 | 여러 번 보거나 인덱싱 필요 | 한 번만 훑거나 매우 큰/무한 데이터 |
무한 수열도 가능
리스트로는 불가능한 "무한 수열"을 제너레이터는 표현할 수 있습니다. 끝없이 만들 수 있지만 필요한 만큼만 꺼내면 되니까요.
def infinite(): n = 0 while True: # 무한 루프지만... yield n n += 1 inf = infinite() print([next(inf) for _ in range(5)]) # [0, 1, 2, 3, 4] (5개만 꺼냄)
피보나치 수열처럼 "필요한 만큼만" 생성하는 데 제격입니다.
def fib(limit): a, b = 0, 1 while a < limit: yield a a, b = b, a + b # 초급편의 동시 할당! print(list(fib(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
```python
g = count_up(3)
print(list(g)) # [1, 2, 3]
print(list(g)) # [] ← 이미 소진! 빈 리스트
```
여러 번 순회해야 한다면 리스트로 받아두거나(
data = list(g)), 제너레이터를 다시 만들어야 합니다.제너레이터 표현식 (복습+연결)
13장에서 본 제너레이터 표현식 (식 for ...)도 제너레이터를 만듭니다. yield 함수의 간단한 버전이라 보면 됩니다.
# 아래 두 줄은 사실상 같은 제너레이터 gen1 = (x ** 2 for x in range(5)) def make_squares(): for x in range(5): yield x ** 2 gen2 = make_squares() print(sum(gen1)) # 30 print(sum(gen2)) # 30
- 간단한 변환·필터라면 → 제너레이터 표현식
(...) - 복잡한 로직(여러 단계, 조건 분기, 무한 수열)이라면 →
yield함수
나쁜 예 ❌ vs 좋은 예 ✅
큰 파일이나 큰 데이터를 한 번 훑을 때, 리스트로 전부 올리는 것은 낭비입니다.
# ❌ 나쁜 예: 1천만 개를 전부 리스트로 만들어 메모리 점유 def get_squares_list(n): result = [] for i in range(n): result.append(i ** 2) return result total = sum(get_squares_list(10_000_000)) # 메모리 많이 씀 # ✅ 좋은 예: 제너레이터로 하나씩 (메모리 거의 안 씀) def get_squares(n): for i in range(n): yield i ** 2 total = sum(get_squares(10_000_000)) # 메모리 효율적
이 장에서 배운 것
for는 내부에서iter()로 이터레이터를 얻고next()를 반복하다StopIteration에서 멈춘다.- 이터러블은 "돌 수 있는 것", 이터레이터는 "값을 하나씩 꺼내주는 장치"다.
yield가 든 함수는 제너레이터가 된다.yield는 값을 내놓고 그 자리에서 멈췄다 이어 달린다.- 제너레이터는 지연 평가로 값을 하나씩 만들어 메모리에 효율적이고, 무한 수열도 표현할 수 있다.
- 제너레이터는 일회용이다. 여러 번 순회하려면 리스트로 받아두어야 한다.
🧪 실습 문제
문제 1. 다음 코드의 출력 순서를 적으세요.
def g(): yield "A" yield "B" yield "C" it = g() print(next(it)) print(next(it))
문제 2. 1부터 n까지 정수를 하나씩 내놓는 제너레이터 함수 up_to(n)을 작성하고, list(up_to(5))를 출력하세요.
문제 3. 다음 코드는 왜 두 번째 출력이 빈 리스트일까요?
nums = (x for x in range(3)) print(list(nums)) print(list(nums))
문제 4. 짝수만 무한히 내놓는 제너레이터 even_numbers()를 작성하고(0, 2, 4, ...), 앞 5개만 리스트로 출력하세요. (힌트: while True + yield)
문제 5. 리스트 [3, 1, 4, 1, 5, 9, 2, 6]에서 3보다 큰 수의 합을 제너레이터 표현식으로 구하세요. (리스트를 따로 만들지 말 것)
<details>
<summary>✅ 정답·해설 보기</summary>
1. A → B. 첫 next가 첫 yield("A")까지 실행하고 멈추고, 둘째 next가 다음 yield("B")까지 실행합니다. "C"는 아직 안 나옵니다.
2.
def up_to(n): for i in range(1, n + 1): yield i print(list(up_to(5))) # [1, 2, 3, 4, 5]
3. 제너레이터는 일회용이기 때문입니다. 첫 list(nums)가 모든 값을 꺼내 소진시켰고, 두 번째는 더 꺼낼 게 없어 []가 됩니다.
4.
def even_numbers(): n = 0 while True: yield n n += 2 gen = even_numbers() print([next(gen) for _ in range(5)]) # [0, 2, 4, 6, 8]
5.
nums = [3, 1, 4, 1, 5, 9, 2, 6] total = sum(n for n in nums if n > 3) print(total) # 24 (4+5+9+6)
</details>
◀️ 이전 장: 13. 컴프리헨션 | ▶️ 다음 장: 15. 모듈과 패키지