728x90
제네레이터의 문제 상황
- 아래와 같이 도시 별 전체 여행자 수 중의 비율 확인하는 함수가 있다고 가정하자
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
visits = [15, 35, 80]
percentages = normalize(visits)
print(percentages) # [11.53434, 26.76767, 61.53434]
- 확장성을 위해 여행자 정보가 들어있는 파일을 읽는다고 가정하자
- 아래의 로직에서는 결과적으로 빈 리스트를 리턴하게 된다.
- 이터레이터가 결과를 단 한 번만 만들어내기 때문이다.
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)
- 이미 StopIteration 예외가 발생한 이터레이터나 제네레이터를 다시 이터레이션 하면 아무 결과도 얻을 수 없다.
it = read_visits('my_numbers.txt')
print(list(it))
print(list(it)) # 이미 모든 원소를 다 소진했다
- 이를 해결하기 위해 입력한 제네레이터를 리스트로 복사하고 해당 로직을 처리한다.
def normalize_copy(numbers):
numbers_copy = list(numbers) # 이터레이터 복사
total = sum(numbers_copy)
result = []
for value in numbers_copy:
percent = 100 * value / total
result.append(percent)
return result
it = read_visits('my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
복사하는 방식의 문제접은 메모리를 엄청나게 사용할 수 있다는 것이다.
새로운 이터레이터를 반환하는 함수
- 사용하게 되면 이터레이터가 존재하지 않으므로 필요할 때 사용하는 것으로 권장한다.
def normalize_func(get_iter):
total = sum(get_iter()) # 새 이터레이터
result = []
for value in get_iter(): # 새 이터레이터
percent = 100 * value / total
result.append(percent)
return result
path = 'my_numbers.txt'
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
assert sum(percentages) == 100.0
이터레이터 프로토콜
- 람다 함수를 사용하는 것은 가독성 상 최선의 방법은 아니다.
- 이를 해결하기 위해 새로운 컨테이너 클래스를 제공하는 방식인 이터레이터 프로토콜을 적용해보자
__iter__내장 함수를 통해 이터레이터 객체를 반환하여 이를 리팩토링 가능하다.- 기본적으로
__iter__내장함수는 이터레이션을 반환한다. - 이를 통해 제네레이터의 독립적인 처리가 가능하다.
- 기본적으로
- 이 방식의 단점은 결국 데이터를 여러 번 읽는다는 것이다.
class ReadVisits:
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
- 추가적으로 컨테이너에 대해서 타입 검사를 실시할 수 있다.
from collections.abc import Iterator
def normalize_defensive(numbers):
if isinstance(numbers, Iterator): # 반복 가능한 이터레이터인지 검사하는 다른 방법
raise TypeError('컨테이너를 제공해야 합니다')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
visits = [15, 35, 80]
percentages = normalize_defensive(visits)
assert sum(percentages) == 100.0
visits = ReadVisits(path)
percentages = normalize_defensive(visits)
assert sum(percentages) == 100.0
visits = [15, 35, 80]
it = iter(visits)
normalize_defensive(it) # 해당 로직에서는 '컨테이너를 제공해야 합니다' 라는 에러가 발생할 것이다.728x90
'Programming Language > Python' 카테고리의 다른 글
| BETTER WAY 36 - 이터레이터나 제너레이터를 다룰 때는 itertools 를 사용하라 (1) | 2024.12.16 |
|---|---|
| BETTER WAY 33 - yield from 를 사용해 여러 제너레이터를 합성해라 (0) | 2024.12.12 |
| BETTER WAY 30 - 리스트를 반환하기보다는 제너레이터를 사용하라 (0) | 2024.12.02 |
| BETTER WAY 26 - functools.wrap 사용해 함수 데코레이터를 정의하라 (0) | 2024.11.26 |
| BETTER WAY 21 - 변수 영역과 클로저의 상호작용 방식을 이해하라 (0) | 2024.11.25 |