본문으로 건너뛰기
Advertisement

이터레이터 프로토콜 — __iter__, __next__, StopIteration

**이터레이터(iterator)**는 Python의 반복 처리 핵심입니다. for 루프가 내부적으로 어떻게 동작하는지, 커스텀 이터레이터를 어떻게 구현하는지 완전히 이해합니다.


이터러블 vs 이터레이터 차이

구분이터러블(Iterable)이터레이터(Iterator)
정의__iter__() 메서드를 가진 객체__iter__()__next__()를 모두 가진 객체
예시list, tuple, str, dict, setenumerate, 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)
Advertisement