컨테이너, 반복형, 반복자 개념 정리
파이썬에서는 데이터를 다루는 다양한 구조가 존재하며, 그 중 컨테이너(container)는 여러 요소를 담을 수 있는 자료구조입니다. 대표적인 예로 리스트, 튜플, 세트, 딕셔너리, 문자열 등이 있습니다.
assert 1 in [1, 2, 3] # 리스트
assert 'a' in {'a', 'b'} # 세트
assert 'x' in "hello world" # 문자열
assert 'key' in {'key': 42} # 딕셔너리 키
컨테이너는 in 연산자를 통해 특정 요소 포함 여부를 확인할 수 있으며, 이는 멤버십 테스트라고 부릅니다. 하지만 모든 컨테이너가 반복 가능한 것은 아니며, 반복 가능하려면 특별한 프로토콜을 구현해야 합니다.
반복 가능한 객체 (Iterable)
반복 가능한 객체란 __iter__() 메서드를 구현하여 반복자(iterator)를 반환할 수 있는 객체입니다. 대부분의 컨테이너는 이 조건을 만족하지만, 모든 반복 가능한 객체가 컨테이너인 것은 아닙니다.
from collections.abc import Iterable
print(isinstance([], Iterable)) # True
print(isinstance("text", Iterable)) # True
print(isinstance(123, Iterable)) # False
반복 가능한 객체는 for 루프에서 사용할 수 있으며, 내부적으로는 매번 iter() 함수를 호출해 반복자를 생성합니다.
반복자 (Iterator)
반복자는 상태를 가진 객체로, __next__() 메서드를 통해 다음 값을 반환하고, 더 이상 값이 없으면 StopIteration 예외를 발생시킵니다. 또한 __iter__() 메서드를 통해 자기 자신을 반환함으로써 반복자이자 반복 가능한 객체가 됩니다.
예를 들어 피보나치 수열을 반복자로 구현하면 다음과 같습니다:
class Fibonacci:
def __init__(self, limit):
self.limit = limit
self.current = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.current >= self.limit:
raise StopIteration
result = self.b
self.a, self.b = self.b, self.a + self.b
self.current += 1
return result
# 사용 예시
fib = Fibonacci(6)
for value in fib:
print(value) # 1, 1, 2, 3, 5, 8 출력
이 방식은 전체 결과를 메모리에 저장하지 않고 필요할 때마다 계산하므로 메모리 효율성이 뛰어납니다.
제너레이터 (Generator)
제너레이터는 반복자를 쉽게 만들 수 있도록 해주는 파이썬의 기능으로, yield 키워드를 사용하여 함수 형태로 작성됩니다. 제너레이터 함수는 호출 시 즉시 실행되지 않고, 대신 제너레이터 객체를 반환합니다.
피보나치 수열을 제너레이터로 구현하면 아래와 같이 간결하게 표현할 수 있습니다:
def fibonacci_gen(n):
a, b = 0, 1
count = 0
while count < n:
yield b
a, b = b, a + b
count += 1
gen = fibonacci_gen(6)
for val in gen:
print(val) # 동일한 출력: 1, 1, 2, 3, 5, 8
제너레이터는 상태를 유지하면서 실행을 일시 중단했다가 재개할 수 있어, 대용량 데이터 처리나 무한 시퀀스 생성에 매우 유용합니다.
제너레이터 표현식
리스트 컴프리헨션과 유사하지만 괄호 ()를 사용하여 제너레이터를 생성할 수 있습니다:
# 리스트 컴프리헨션 – 모든 결과를 메모리에 저장
squares_list = [x * x for x in range(10)]
# 제너레이터 표현식 – 필요할 때마다 값을 생성
squares_gen = (x * x for x in range(10))
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>
제너레이터 표현식은 메모리를 절약하면서도 반복 가능한 인터페이스를 제공합니다.
내장 제너레이터 함수
파이썬 표준 라이브러리에는 다양한 제너레이터 기반 함수들이 포함되어 있습니다:
- zip(): 여러 반복 가능 객체를 병렬로 순회
- map(): 함수를 각 요소에 적용
- dict.items(): 키-값 쌍을 제너레이터로 반환
- itertools 모듈: 무한 시퀀스 생성기 제공
from itertools import count, cycle
counter = count(start=10)
print(next(counter)) # 10
print(next(counter)) # 11
colors = cycle(['red', 'green', 'blue'])
print(next(colors)) # red
print(next(colors)) # green
코루틴과 send(), throw(), close()
제너레이터는 단순히 값을 내보내는 것을 넘어, 외부로부터 값을 받거나 예외를 처리하는 코루틴(coroutine)으로도 활용될 수 있습니다.
def echo():
while True:
received = yield
if received == "quit":
break
print(f"Received: {received}")
e = echo()
next(e) # 제너레이터 초기화
e.send("Hello") # Received: Hello
e.send("World") # Received: World
e.send("quit") # 종료
주요 메서드:
send(value): 값을 전달하고 다음yield까지 실행throw(exception): 제너레이터 내부에 예외 발생close(): 제너레이터 종료 및 정리 작업 수행
제너레이터의 장점
- 메모리 효율성: 전체 데이터셋을 로드하지 않고 필요한 순간에만 계산
- 지연 평가(lazy evaluation): 값이 요청될 때 비로소 계산
- 무한 시퀀스 표현 가능: 메모리 제약 없이 무한한 데이터 스트림 생성
- 코드 간결성: 복잡한 상태 관리 없이 자연스러운 흐름으로 로직 구현
주의사항
- 제너레이터는 한 번 소비되면 재사용 불가
- 이전 상태로 돌아갈 수 없음 (단방향)
- 동시에 여러 번 반복하려면 새로운 제너레이터 인스턴스 생성 필요
- 딕셔너리 변경 중에는
.items()반복자가 오류를 발생시킬 수 있음
실제 활용 사례: 파일 스트리밍 읽기
대용량 파일을 메모리에 올리지 않고 블록 단위로 처리할 수 있습니다:
def read_in_blocks(file_path, block_size=1024):
with open(file_path, 'rb') as f:
while True:
block = f.read(block_size)
if not block:
break
yield block
# 사용 예시
for chunk in read_in_blocks('large_file.bin'):
process(chunk) # 각 청크를 처리
이처럼 제너레이터는 성능과 확장성을 고려한 시스템 설계에 핵심적인 역할을 합니다.