이터레이터를 반환하는 함수
yield 키워드 사용
요구사항 : 구매 정보가 저장된 대용량 CSV파일 ➡️ 최저가
, 최고가
, 평균가
추출
class PurchaseStatus:
def __init__(self, purchases):
self.purchases = iter(purchases)
self.min_price: float = None
self.max_price: float = None
self._total_purchases_price: float = 0.0
self._total_purchases = 0
self._initialize()
def process(self):
for purchase_value in self.purchases:
self._update_min(purchase_value)
self._update_max(purchase_value)
self._update_avg(purchase_value)
return self
def _initialize(self):
try:
first_value = next(self.purchases)
except StopIteration:
raise ValueError("더 이상 값이 없음")
self.min_price = self.max_price = first_value
self._update_avg(first_value)
def _update_min(self, new_value):
if new_value < self.min_price:
self.min_price = new_value
def _update_max(self, new_value):
if new_value > self.max_price:
self.max_price = new_value
def avg_price(self):
return self._total_purchases_price / self._total_purchases
def _update_avg(self, new_value):
self._total_purchases_price += new_value
self._total_purchases += 1
def __str__(self):
return f"{self.__class__.__name__}({self.min_price}, {self.max_price}, {self.avg_price})"
PurchaseStatus 클래스의 컨스트럭터 인자로 purchases가 필요하다.
(1) 메모리 사용량이 많은 코드
"모든 구매 정보를 일단 다 받아야겠어"
def load_purchases(filename):
purchases = []
with open(filename) as f:
for line in f:
*_, price_raw = line.partition(",")
purchases.append(float(price_raw))
return purchases
(2) 메모리 사용량이 적은 코드
"그때그때 필요한 값만 가져와"
def load_purchases_2(filename):
with open(filename) as f:
for line in f:
*_, price_raw = line.partition(",")
yield float(price_raw)
(누군가는 제너레이터 컴프리헨션으로 불러야 한다고 주장한다.)
(1) 리스트 컴프리헨션
>>> [x**2 for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
(2) 제너레이터 표현식
>>> (x**2 for x in range(10))
<generator object <genexpr> at 0x102eaa4d0>
(3) 이터러블을 파라미터로 받는 함수와의 조합
>>> sum(x**2 for x in range(10))
285
>>> max(x**2 for x in range(10))
81
>>> min(x**2 for x in range(10))
0
>>> sum([x**2 for x in range(10)])
285
제너레이터는 반복을 완료하면 소모(exhausted)된다. 왜냐하면 모든 데이터를 메모리에 로드하지 않기 때문이다.
>>> list(enumerate("abcdef"))
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')]
class NumberSequence:
def __init__(self, start=0):
self.current = start
def next(self):
current = self.current
self.current += 1
return current
>>> seq = NumberSequence()
>>> seq.next()
1
>>> seq.next()
2
>>> seq.next()
3
>>> seq = NumberSequence(10)
>>> seq.next()
11
>>>
>>> seq.next()
12
NumberSequence는 반복을 지원하지 않는다.
>>> list(zip(NumberSequence(), "abcdef"))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NumberSequence' object is not iterable
__iter__
매직 메서드를 구현하여 객체가 반복 가능하게 만들어야 한다.
class NumberSequence:
def __init__(self, start=0):
self.current = start
def __next__(self):
current = self.current
self.current += 1
return current
def __iter__(self):
return self
>>> list(zip(NumberSequence(), "abcdef"))
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')]
>>> NumberSequence(100)
<__main__.NumberSequence object at 0x10af2dfa0>
>>> next(seq)
100
>>> next(seq)
101
요소를 반복할 수 있을 뿐만 아니라,
.next()를 호출할 필요도 없다.
이터레이터 프로토콜은 __iter__
와 __next__
메서드에 의존한다.
next() 내장 함수는 이터레이터를 다음 요소로 이동시키고 기존의 값을 반환한다.
>>> word = iter("hello")
>>> word
<str_iterator object at 0x10af2dfa0>
>>> next(word)
'h'
>>> next(word)
'e'
>>> next(word)
'l'
>>> next(word)
'l'
>>> next(word)
'o'
>>> next(word)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
이터레이터가 더 이상의 값을 가지고 있지 않다면 StopIteration 예외가 발생한다.
SteopIteration 에러를 캐치하는 것 외에도 두 번째 파라미터에 기본값을 제공할 수도 있다.
>>> next(word, "default_value")
'default_value'
>>> next(word, "default_value")
'default_value'
>>> next(word, "default_value")
def sequence(start=0):
while 1:
yield start
start += 1
>>> seq = sequence(10)
>>> next(seq)
10
>>> next(seq)
11
>>> next(seq)
12
>>> list(zip(sequence(), "abcdef")
...
... )
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')]
>>> list(zip(sequence(5), "abcdef"))
[(5, 'a'), (6, 'b'), (7, 'c'), (8, 'd'), (9, 'e'), (10, 'f')]
>>>
이터러블을 사용하여 구현할 수도 있지만, 제너레이터로 구현하는 것을 권장한다.(간단한 구조 / 가독성)
위에서 다뤘던 코드(구매 정보가 저장된 대용량 CSV파일 ➡️ 최저가
, 최고가
, 평균가
추출)에 추가 요구사항이 생겼다.
1000원을 넘는 구매에 대해서만 값들을 추출하려고 한다.
class PurchaseStatus:
...
def process(self):
for purchase in purchases:
if purchase > 1000.0:
...
그러나 위 방식은 나쁜 코드다.
조건을 필터링하는 것은 PurchaseStatus
클래스의 책임이 아니다.
-> 클라이언트의 요구로부터 독립되어야 한다.(이 클래스의 책임이 작을수록 클라이언트는 재사용성이 높아진다.)
-> 앞으로 변경에 취약한 클래스가 된다.
어떻게 하면 될까?
1000개 넘게 구매한 이력의 처음 10개에 대해서만 값을 추출
>>> from itertools import islice
>>> purchases = islice(filter(lambda p: p > 1000.0, purchases), 10)
>>> stats = PurchasesStatus(purchases).process()
def process_purchases(purchases):
min_, max_, avg = itertools.tee(purchases, 3)
return min(min_), max(max_), median(avg)
itertools.tee는 원래의 이터러블을 세 개의 새로운 이터레이터로 분할한다.
참고) itertools tee
from itertools import tee
i1, i2, i3 = tee(range(10), 3)
>>> i1
<itertools._tee object at 0x104668380>
>>> i2
<itertools._tee object at 0x1046684c0>
>>> i3
<itertools._tee object at 0x104668500>
>>> list(i1)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(i1)
[]
>>> list(i2)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(i2)
[]
>>> list(i3)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(i3)
[]
tee는 마치 제너레이터를 사용하는 for문이 여러 개 있다고 볼 수 있다.
for 문이 3개가 있다고 해서 메모리 사용량이 기존 대비 3배가 되는 것은 아니다. 다만, 이터러블을 여러 개 생성한다면 성능상의 불리함을 가져온다.O(N) 또한, 불필요하게 이터레이터를 생성하는 것은 아닌지 판단해야 한다. 리스트에 한 번에 가져와 순회하는 게 더 효율적일 수도 있다.
다음은 피해야 할 코드다.
종료 플래그를 사용한 이중 for문 탈출 방식이다.
def search_nested_bad(array, desired_value):
coords = None
for i, row in enumerate(array):
for j, cell in enumerate(row):
if cell == desired_value:
coords = (i, j)
break
if coords is not None:
break
if coords is None:
raise ValueError(f"{desired_value} 값을 찾을 수 없음")
return coords
다음은 제너레이터를 활용하여 반복을 추상화한 방식이다.
def _iterate_array2d(array2d):
for i, row in enumerate(array2d):
for j, cell in enumerate(row):
yield (i, j), cell
def search_nested(array, desired_value):
try:
coord = next(
coord
for (coord, cell) in _iterate_array2d(array)
if cell == desired_value
)
except StopIteration:
raise ValueError(f"{desired_value} 값을 찾을 수 없음")
return coord
특정 시점에 실행을 일시 중단했다가 나중에 재시작할 수있는 함수를 만드는 것
따라서, 메인 루틴과 서브루틴이 대응한 관계로 협력할 수 있게 된다.
이 메서드를 호출하면 제너레이터에서 GeneratorExit
예외가 발생한다.