Source
전문가를 위한 파이썬(Fluent Python) 17장
Iterable, Iterator
반복형(Iterable)과 반복자(Iterator)
- 
정의
- 반복형(Iterable): for 루프에서 쓸 수 있는 객체. iter() 함수를 적용할 수 있는 객체.
 - 반복자(Iterator): next() 메서드로 다음 값을 계속 꺼낼 수 있는 객체. 끝에 도달하면 StopIteration 예외를 발생시킴.
 
 - 
둘의 관계: 반복형은 반복자를 생성하는 객체임. 즉 반복형은
__iter__()메서드를 구현하고, 이 메서드는 반복자(iterator)를 리턴 해야 함. - 
iter() 내장함수란?
- 파이썬이 반복을 시작할 때 호출하는 함수로 다음 2가지 동작을 함
- 객체에 
__iter__()가 있으면 그걸 호출해서 반복자를 리턴. - 없으면, 
__getitem__()이 구현되어 있는 경우, 인덱스 0부터 순차적으로 값을 꺼내 반복자를 흉내냄. 인덱스 오류가 나면 멈춤- 이게 바로 모든 시퀀스가 반복형인 이유 → str, list, tuple 같은 시퀀스 객체는 
__getitem__()을 통해 인덱스로 접근할 수 있어서 반복이 지원되기 때문 
 - 이게 바로 모든 시퀀스가 반복형인 이유 → str, list, tuple 같은 시퀀스 객체는 
 
 - 객체에 
 
 - 파이썬이 반복을 시작할 때 호출하는 함수로 다음 2가지 동작을 함
 
import re
import reprlib
 
RE_WORD = re.compile(r'\w+')
 
 
class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)
 
    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'
 
    def __iter__(self):
        return SentenceIterator(self.words)  
 
 
class SentenceIterator:
    def __init__(self, words):
        self.words = words  
        self.index = 0  
    def __next__(self):
        try:
            word = self.words[self.index]  
        except IndexError:
            raise StopIteration() 
        self.index += 1  
        return word  
    def __iter__(self): 
        return self- 
Iterator는 기본적으로 위 예시와 같이 index의 상태를 카운트함.
__next__를 구현해야 함 = 다음 값을 꺼내는 함수. 다음 값을 꺼내고, index를 하나 올리고, index가 끝까지 가면 StopIteration 예외를 발생시킴.- Iterator에서도 
__iter__를 구현해야 함 = for 루프 등 반복형이 필요한 곳에 사용되려면 Iterator 자신도 iterable이어야 하기 때문에. 자기 자신을 돌려주도록 구현. 
 - 
__getitem__을 써도 반복형으로 잘 동작하는데 왜 Iterator를 제대로 구현해야 하나?__getitem__의 단점은 한번에 메모리에 모든 데이터를 올려야 하고, 내부 상태가 없는 반복 → 매 반복마다 처음부터 시작- 위와 같은 구현의 장점
- 지연 평가(lazy evaluation) 가능 → 즉 필요한 만큼만 가져와서 필요한 만큼만 계산한다 (메모리 절약)
- lazy의 반대말은 eager다..
 
 - 내부 상태(index 등)를 갖고 있어서 복잡한 로직 구현 가능
 - 무한 반복자, 파일 스트리밍 등 동적이고 유연한 반복 처리 가능
 - 여러 개의 반복자를 병렬로 써도 각각 독립적으로 동작
 
 - 지연 평가(lazy evaluation) 가능 → 즉 필요한 만큼만 가져와서 필요한 만큼만 계산한다 (메모리 절약)
 - 예를 들면
- 대용량 텍스트 파일에서 줄 단위로 처리하고 싶을 때
 - 네트워크에서 스트리밍으로 데이터를 읽을 때
 - 한 번만 순회 가능한 객체 (예: 제너레이터) 등을 쓸 때
 
 
 - 
한번에
__iter__()과__next__()를 구현하는 것은 (즉 위 예시에서 SentenceIterator를 안만들고 Sentence에 때려넣기) 안티패턴임 - 
이유: 반복을 한번만 하면 끝나버림. 재사용 불가. 병렬 반복이 안됨
 
s = Sentence("this is not good")
for w in s:
    print(w)  # OK
for w in s:
    print(w)  # 아무 것도 안 나옴 (index가 이미 끝에 있음)
 
it1 = iter(s)
it2 = iter(s)  # 같은 객체 리턴됨 → 상태 공유됨
next(it1)  # it2도 영향을 받음
 Generator
Sentence에서 Iterator를 별도로 만들지 않고 __iter__를 다음과 같이 쓴다면?
def __iter__(self):
    for word in self.words:
        yield word- 
= 제너레이터
- yield 구문을 사용해서 값을 하나씩 생성하는 특별한 함수나 표현식을 의미함
 - return 대신 yield를 사용해서 값을 하나씩 반환하고, 호출자가 next()로 요구할 때마다 중단했던 지점에서 이어서 실행
 - 반복자를 자동으로 만들어주는 문법적 편의 기능으로 이해..
- 제너레이터 객체는 
__next__를 제공하므로 반복자임 
 - 제너레이터 객체는 
 
 - 
사실 이터레이터의 장점이 lazy할 수 있다는 거라고 했지만, 위의 예시들은 lazy하지 않았음. findall에서 이미 다 메모리에 올려놓고 순회를 시작하기 때문.
 - 
lazy하게 만든다면?
 
def __iter__(self):
    for match in RE_WORD.finditer(self.text):
        yield match.group()- finditer로 매칭되는 단어를 하나씩 돌려줌
 - 텍스트가 커도 CPU, 메모리 자원을 효율적으로 사용
 
def __iter__(self):
    return (match.group() for match in RE_WORD.finditer(self.text))- 동일한 내용을 제너레이터 표현식으로 작성했음
- 로직이 간단하고 짧다면 가독성이 높음
 - 여러 줄로 걸칠 거면 그냥 제너레이터 함수 쓰자
 
 
표준 라이브러리 제너레이터
Filtering
| 함수 | 설명 | 예시 | 
|---|---|---|
itertools.compress(data, selectors) | selectors가 True인 곳의 data만 반환 | compress('ABCDE', [1,0,1,0,1]) → A C E | 
itertools.dropwhile(pred, iterable) | 조건이 False가 되는 시점부터 모든 값 반환 | dropwhile(lambda x: x<3, [1,2,3,4]) → 3 4 | 
itertools.filterfalse(pred, iterable) | 조건이 False인 값만 반환 | filterfalse(lambda x: x%2, range(5)) → 0 2 4 | 
filter(pred, iterable) | 조건이 True인 값만 반환 | filter(lambda x: x>3, [1,4,5]) → 4 5 | 
itertools.takewhile(pred, iterable) | 조건이 False가 되기 전까지 값 반환 | takewhile(lambda x: x<3, [1,2,3,4]) → 1 2 | 
Mapping
| 함수 | 설명 | 예시 | 
|---|---|---|
itertools.accumulate(iterable, func=operator.add) | 누적 계산 (합/곱/최대 등) | accumulate([1,2,3]) → 1 3 6 | 
enumerate(iterable, start=0) | (인덱스, 값) 튜플 반환 | enumerate('abc') → (0, 'a'), (1, 'b') | 
map(func, iterable) | 각 요소에 함수 적용 | map(str.upper, ['a', 'b']) → 'A' 'B' | 
itertools.starmap(func, iterable_of_tuples) | 튜플을 언팩해서 함수에 적용 | starmap(pow, [(2,3),(3,2)]) → 8 9 | 
Merging
| 함수 | 설명 | 예시 | 
|---|---|---|
itertools.chain(*iterables) | 여러 iterable을 하나처럼 이어 붙임 | chain('AB', 'CD') → A B C D | 
itertools.product(*iterables, repeat=1) | 데카르트 곱 | product('AB', repeat=2) → AA AB BA BB | 
zip(a, b) | 같은 인덱스끼리 튜플 묶음 (짧은 쪽 기준) | zip('AB', '12') → ('A','1'), ('B','2') | 
itertools.zip_longest(a, b, fillvalue=None) | zip과 같지만 긴 쪽 기준, 없는 곳은 fill | zip_longest('AB', '123', fillvalue='X') → ('A','1'), ('B','2'), (None,'3') | 
Expanding
| 함수 | 설명 | 예시 | 
|---|---|---|
itertools.combinations(iterable, r) | r개 조합 (순서 무관) | combinations('ABC', 2) → AB AC BC | 
itertools.count(start=0, step=1) | 무한 증가 수열 | count(10, 2) → 10 12 14 ... | 
itertools.cycle(iterable) | 반복적으로 순환 | cycle('AB') → A B A B A ... | 
itertools.pairwise(iterable) | (현재, 다음) 쌍 튜플 생성 | pairwise('ABCD') → (A,B), (B,C), (C,D) | 
itertools.permutations(iterable, r=None) | r개 순열 (순서 중요) | permutations('ABC', 2) → AB AC BA BC CA CB | 
itertools.repeat(elem, times=None) | 같은 값을 계속 반복 | repeat(10, 3) → 10 10 10 | 
Rearranging
| 함수 | 설명 | 예시 | 
|---|---|---|
itertools.groupby(iterable, key=...) | 인접한 값 기준 그룹핑 | groupby('AAABBB') → ('A', ['A','A','A']), ('B', ['B','B','B']) | 
reversed(seq) | 역순 반복자 (list, str 등 시퀀스만) | reversed([1,2,3]) → 3 2 1 | 
itertools.tee(iterable, n=2) | 반복 가능한 객체를 n개 복제 | a, b = tee(range(3)) → a, b 독립 사용 가능 | 
Reduce
| 함수 | 설명 | 예시 | 
|---|---|---|
all(iterable) | 모두 True여야 True | all([1, 2, 3]) → True | 
any(iterable) | 하나라도 True면 True | any([0, 0, 3]) → True | 
max(iterable) / min(...) | 최댓값 / 최솟값 | max([1, 5, 2]) → 5 | 
functools.reduce(func, iterable[, initializer]) | 누적 계산 후 최종값 하나 반환 | reduce(lambda x,y: x+y, [1,2,3]) → 6 | 
sum(iterable) | 합계 | sum([1, 2, 3]) → 6 | 
Classic Coroutines
- 
코루틴은 “함수 ↔ 호출자” 간에 양방향으로 값을 주고받을 수 있는 실행 단위
- 일반 함수: 호출자 → 함수 → return
 - 제너레이터: 호출자 → 함수 → yield
 - 코루틴: 호출자 ↔ 함수 ←→ send/yield
 
 - 
고전적 코루틴이란
- async/await 문법이 없었을 때 제너레이터에 yield + .send()를 조합 해서 비슷하게 동작하게 했던 것 (파이썬 3.5 이전)
 
 
def package_receiver():
    while True:
        package = yield 
        print(f"택배를 받았다: {package}")
 
receiver = package_receiver()
next(receiver) # 코루틴 가동 (yield까지 실행)
receiver.send("의류")  
receiver.send("전자기기")  - yield에서 멈추고, 이후에 send를 통해 외부에서 값을 받아서 하나씩 반복해서 동작하는 방식