Python 기초부터 마스터까지 — 7화
이터레이터, 생성기, 컨텍스트 관리자로 데이터 흐름 다루기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
대용량 데이터를 모두 리스트로 만들 필요는 없다. Python의 반복 프로토콜과 생성기를 사용하면 값이 필요할 때 하나씩 계산하고 자원의 수명도 명시적으로 관리할 수 있다.
iterable과 iterator
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator)) # 10
print(next(iterator)) # 20
리스트는 여러 번 순회할 수 있는 iterable이다. iter(numbers)가 만든 iterator는 현재 위치를 가진 일회성 객체이며 더 꺼낼 값이 없으면 StopIteration으로 끝난다.
for문은 이 과정을 직접 처리한다. 같은 iterator를 두 코드가 나눠 쓰면 서로의 진행 위치에 영향을 주므로 소유 관계를 분명히 해야 한다.
yield로 생성기 만들기
from collections.abc import Iterator
def retry_delays(attempts: int) -> Iterator[float]:
delay = 0.5
for _ in range(attempts):
yield delay
delay *= 2
for delay in retry_delays(4):
print(delay)
출력은 0.5, 1.0, 2.0, 4.0 순서다. 생성기 함수는 호출하는 순간 본문을 끝까지 실행하지 않고 다음 값이 필요할 때 yield까지 실행한 뒤 상태를 보존한다.
큰 파일을 한 줄씩 읽기
from collections.abc import Iterator
from pathlib import Path
def nonempty_lines(path: Path) -> Iterator[str]:
with path.open("r", encoding="utf-8") as file:
for line in file:
text = line.strip()
if text:
yield text
파일 전체를 read()로 읽지 않아도 한 줄씩 처리할 수 있다. 다만 생성기를 소비하는 동안 파일도 열려 있다. 일부만 읽고 중단한다면 생성기가 언제 닫히는지까지 API의 계약으로 생각해야 한다.
컨텍스트 관리자로 수명 묶기
from collections.abc import Iterator
from contextlib import contextmanager
from time import perf_counter
@contextmanager
def measured(label: str) -> Iterator[None]:
started = perf_counter()
try:
yield
finally:
elapsed = perf_counter() - started
print(f"{label}: {elapsed:.6f}s")
with measured("load"):
total = sum(int(line) for line in nonempty_lines(Path("numbers.txt")))
with 블록이 끝나면 정상 실행과 예외 발생 모두에서 finally가 실행된다. 파일, 잠금, 데이터베이스 연결, 임시 설정처럼 시작과 정리가 항상 짝을 이뤄야 하는 작업에 잘 맞는다.
생성기는 무조건 빠르게 만드는 기능이 아니다. 메모리를 줄이는 대신 한 번만 순회할 수 있고 자원 수명과 오류 시점이 달라질 수 있으므로 실제 데이터 크기와 사용 방식에 맞게 선택해야 한다.