이터레이터 프로토콜 — __iter__, __next__, StopIteration
**이터레이터(iterator)**는 Python의 반복 처리 핵심입니다. for 루프가 내부적으로 어떻게 동작하는지, 커스텀 이터레이터를 어떻게 구현하는지 완전히 이해합니다.
이터러블 vs 이터레이터 차이
| 구분 | 이터러블(Iterable) | 이터레이터(Iterator) |
|---|---|---|
| 정의 | __iter__() 메서드를 가진 객체 | __iter__()와 __next__()를 모두 가진 객체 |
| 예시 | list, tuple, str, dict, set | enumerate, zip, map, filter, generator |
| 특징 | 여러 번 반복 가능 | 한 번만 소비 가능, 상태 유지 |
| 관계 | iter() 호출 시 이터레이터 반환 | 이터러블이기도 함(자기 자신 반환) |
# 이터러블: 여러 번 반복 가능
my_list = [1, 2, 3]
for x in my_list: # 첫 번째 반복
pass
for x in my_list: # 두 번째 반복 — 처음부터 다시 시작
print(x) # 1, 2, 3
# 이터레이터: 한 번 소비
my_iter = iter(my_list)
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
print(next(my_iter)) # StopIteration!
# 이터레이터는 이터러블이기도 함
my_iter2 = iter(my_list)
for x in my_iter2: # 이터레이터에 직접 for 가능
print(x)
# 다시 반복하면 아무것도 없음 (소진됨)
for x in my_iter2:
print(x) # 출력 없음
__iter__ / __next__ 프로토콜
# 이터러블 프로토콜
class MyIterable:
def __iter__(self):
"""이터레이터를 반환"""
return MyIterator(self.data)
# 이터레이터 프로토콜
class MyIterator:
def __iter__(self):
"""이터레이터 자신을 반환 (이터러블이기도 함)"""
return self
def __next__(self):
"""다음 값 반환, 소진되면 StopIteration"""
...
StopIteration 예외
이터레이터가 모든 값을 반환했을 때 StopIteration을 발생시켜 반복 종료를 알립니다.
class CountDown:
"""카운트다운 이터레이터"""
def __init__(self, start: int):
self.current = start
def __iter__(self):
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
countdown = CountDown(3)
for n in countdown:
print(n) # 3, 2, 1
iter() / next() 내장 함수
# iter(iterable) — 이터레이터 반환
it = iter([10, 20, 30])
print(next(it)) # 10
print(next(it)) # 20
# next(iterator, default) — 기본값 지정 (StopIteration 방지)
print(next(it)) # 30
print(next(it, "끝")) # "끝" (StopIteration 대신)
# iter(callable, sentinel) — 두 인자 형식
import random
# sentinel 값이 나올 때까지 callable 반복 호출
roll_until_six = iter(lambda: random.randint(1, 6), 6)
for n in roll_until_six:
print(f"주사위: {n}")
print("6이 나왔습니다!")
# 파일을 줄 단위로 읽는 실용 패턴
with open("data.txt") as f:
# iter(callable, sentinel) 패턴으로 파일 읽기
for line in iter(f.readline, ""):
process(line)
커스텀 이터레이터 클래스 구현
예제 1: 범위 이터레이터
class Range:
"""내장 range()와 유사한 커스텀 범위 이터레이터"""
def __init__(self, start: int, stop: int, step: int = 1):
if step == 0:
raise ValueError("step은 0이 될 수 없습니다.")
self.start = start
self.stop = stop
self.step = step
def __iter__(self) -> "RangeIterator":
return RangeIterator(self.start, self.stop, self.step)
def __len__(self) -> int:
if self.step > 0:
return max(0, (self.stop - self.start + self.step - 1) // self.step)
else:
return max(0, (self.start - self.stop - self.step - 1) // (-self.step))
def __repr__(self) -> str:
return f"Range({self.start}, {self.stop}, {self.step})"
class RangeIterator:
def __init__(self, start: int, stop: int, step: int):
self.current = start
self.stop = stop
self.step = step
def __iter__(self) -> "RangeIterator":
return self
def __next__(self) -> int:
if (self.step > 0 and self.current >= self.stop) or \
(self.step < 0 and self.current <= self.stop):
raise StopIteration
value = self.current
self.current += self.step
return value
# 이터러블과 이터레이터 분리: 여러 번 반복 가능
r = Range(1, 10, 2)
print(list(r)) # [1, 3, 5, 7, 9]
print(list(r)) # [1, 3, 5, 7, 9] — 다시 사용 가능!
print(len(r)) # 5
예제 2: 피보나치 이터레이터
class Fibonacci:
"""무한 피보나치 수열 이터레이터"""
def __init__(self, limit: int | None = None):
self.limit = limit
def __iter__(self) -> "FibIterator":
return FibIterator(self.limit)
class FibIterator:
def __init__(self, limit: int | None):
self.a, self.b = 0, 1
self.limit = limit
self.count = 0
def __iter__(self) -> "FibIterator":
return self
def __next__(self) -> int:
if self.limit is not None and self.count >= self.limit:
raise StopIteration
value = self.a
self.a, self.b = self.b, self.a + self.b
self.count += 1
return value
# 처음 10개 피보나치 수
print(list(Fibonacci(10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 무한 피보나치에서 100 미만만 취하기
import itertools
fib_under_100 = list(itertools.takewhile(lambda x: x < 100, Fibonacci()))
print(fib_under_100) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
예제 3: 파일 청크 이터레이터
from pathlib import Path
class ChunkReader:
"""대용량 파일을 고정 크기 청크로 읽는 이터레이터"""
def __init__(self, filepath: str | Path, chunk_size: int = 4096):
self.filepath = Path(filepath)
self.chunk_size = chunk_size
def __iter__(self) -> "ChunkReaderIterator":
return ChunkReaderIterator(self.filepath, self.chunk_size)
class ChunkReaderIterator:
def __init__(self, filepath: Path, chunk_size: int):
self.chunk_size = chunk_size
self._file = open(filepath, "rb")
def __iter__(self) -> "ChunkReaderIterator":
return self
def __next__(self) -> bytes:
chunk = self._file.read(self.chunk_size)
if not chunk:
self._file.close()
raise StopIteration
return chunk
def __del__(self):
"""소멸 시 파일 닫기"""
if hasattr(self, "_file") and not self._file.closed:
self._file.close()
# 사용
for chunk in ChunkReader("large_file.bin"):
process_chunk(chunk)
for 루프의 내부 동작
# for 루프의 실제 동작
my_list = [1, 2, 3]
for x in my_list:
print(x)
# 위 코드는 내부적으로 이렇게 동작합니다:
_iter = iter(my_list) # __iter__() 호출
while True:
try:
x = next(_iter) # __next__() 호출
print(x)
except StopIteration:
break # 반복 종료
# in 연산자도 이터레이터 프로토콜 사용
# x in iterable → 내부적으로 순차 탐색
print(3 in [1, 2, 3, 4, 5]) # True — O(n) 탐색
print(3 in {1, 2, 3, 4, 5}) # True — O(1) 해시 탐색
# list(iterable) — 이터레이터를 소진하며 리스트 생성
from io import StringIO
f = StringIO("line1\nline2\nline3")
lines = list(f) # ['line1\n', 'line2\n', 'line3']
실전 예제: CSV 페이징 이터레이터
import csv
from pathlib import Path
from typing import Iterator
class CsvPageIterator:
"""CSV 파일을 페이지 단위로 읽는 이터레이터"""
def __init__(self, filepath: str | Path, page_size: int = 100):
self.filepath = Path(filepath)
self.page_size = page_size
def __iter__(self) -> Iterator[list[dict]]:
page: list[dict] = []
with open(self.filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
page.append(dict(row))
if len(page) >= self.page_size:
yield page
page = []
if page: # 마지막 페이지
yield page
def __len__(self) -> int:
"""총 페이지 수 (파일 전체 스캔 필요)"""
total_rows = sum(1 for _ in open(self.filepath)) - 1 # 헤더 제외
return (total_rows + self.page_size - 1) // self.page_size
# 사용
pager = CsvPageIterator("users.csv", page_size=50)
for page_num, page in enumerate(pager, 1):
print(f"페이지 {page_num}: {len(page)}행 처리")
for row in page:
process_row(row)
고수 팁
팁 1: 이터러블과 이터레이터를 분리하면 재사용 가능
# 나쁜 예: 이터러블과 이터레이터를 같은 클래스에 구현
class BadRange:
def __iter__(self): return self
# 한 번 소진되면 재사용 불가
# 좋은 예: 분리
class GoodRange:
def __iter__(self): return GoodRangeIterator(...)
class GoodRangeIterator:
def __iter__(self): return self
def __next__(self): ...
팁 2: __length_hint__로 힌트 제공
class MyIterator:
def __init__(self, data):
self.data = data
self.index = 0
def __length_hint__(self) -> int:
"""남은 요소 수 힌트 (정확하지 않아도 됨)"""
return len(self.data) - self.index
팁 3: iter()의 두 인자 형식 활용
import struct
# 바이너리 파일을 4바이트씩 읽기
with open("data.bin", "rb") as f:
for chunk in iter(lambda: f.read(4), b""):
value = struct.unpack("I", chunk)[0]
print(value)