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.538461538461538, 26.923076923076923, 61.53846153846154]
assert sum(percentages) == 100.0
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)
[]
이런 현상이 일어난 이유는 이터레이터가 결과를 단 한번만 만들어내기 때문이다
it = read_visits('my_numbers.txt')
print(list(it))
[15, 35, 80]
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)
[11.538461538461538, 26.923076923076923, 61.53846153846154]
하지만 이러한 방식은 이터레이터의 내용을 복사하여 메모리를 많이 사용하게 된다.
이 문제를 해결하는 다른방법은 호출될 떄마다 새로 이터레이터를 반환하는 함수를 받는 것이다.
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)
[11.538461538461538, 26.923076923076923, 61.53846153846154]
하지만 람다함수를 넘기는 것은 보기에 좋지 않다.
같은 결과를 달성하는 더 나은 방법은 이터레이터 프로토콜을 구현한 새로운 컨테이너 클래스를 제공하는 것이다.
이터레이터 프로토콜은 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차다.
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)
[11.538461538461538, 26.923076923076923, 61.53846153846154]
이 코드가 정삭 작동 하는 이유는 normalizer 함수 안의 sum 메서드와 for 루프가 iter 를 호출해서 각각 객체를 만들기 때문이다.
이 접근법의 유일한 단점은 입력 데이터를 여러번 읽는다는 것이다.
아래는 반복적으로 이터레이션할 수 없는 인자인 경우에는 TypeError를 발생시켜줄 수 있다.
def normalize_defensive(numbers):
if iter(numbers) is numbers: # 이터레이터 -- 나쁨!
raise TypeError('컨테이너를 제공해야 합니다')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
normalize_defensive([1,2,3])
[16.666666666666668, 33.333333333333336, 50.0]
다른 대안으로 collections.abc 내장 모듈은 isinstance를 사용해 잠재적인 문제를 검사할 수 있는 Iterator 클래스를 제공한다.
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)
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]
visits = ReadVisits(path)
percentages = normalize_defensive(visits)
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]
visits = [15, 35, 80]
it = iter(visits)
percentages = normalize_defensive(it)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-24-fc30947d22fc> in <module> 1 visits = [15, 35, 80] 2 it = iter(visits) ----> 3 percentages = normalize_defensive(it) <ipython-input-20-6a9b815bc910> in normalize_defensive(numbers) 1 def normalize_defensive(numbers): 2 if isinstance(numbers, Iterator): ----> 3 raise TypeError('컨테이너를 제공해야 합니다') 4 5 total = sum(numbers) TypeError: 컨테이너를 제공해야 합니다