#!/usr/bin/env python # coding: utf-8 # ## 31. 인자에 대해 이터레이션할 떄는 방어적이 돼라 # In[1]: def normalize(numbers): total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result # In[2]: visits = [15, 35, 80] percentages = normalize(visits) print(percentages) # In[3]: assert sum(percentages) == 100.0 # In[4]: def read_visits(data_path): with open(data_path) as f: for line in f: yield int(line) # In[5]: it = read_visits('my_numbers.txt') percentages = normalize(it) print(percentages) # 이런 현상이 일어난 이유는 이터레이터가 결과를 단 한번만 만들어내기 때문이다 # In[6]: it = read_visits('my_numbers.txt') # In[7]: print(list(it)) # In[8]: print(list(it)) # 이런 경우 이터레이터와 이미 소진돼버린 것을 구분하기 어렵다. # # 이 문제를 해결하기 위해 입력 이터레이터를 명시적으로 소진시키고 이터레이터의 전체 내용을 리스트에 넣을 수 있다. # # 그 후 데이터를 담아둔 리스트에 대해 원하는 수만큼 이터레이션을 수행할 수 있다. # In[9]: 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 # In[11]: it = read_visits('my_numbers.txt') percentages = normalize_copy(it) print(percentages) # 하지만 이러한 방식은 이터레이터의 내용을 복사하여 메모리를 많이 사용하게 된다. # # 이 문제를 해결하는 다른방법은 호출될 떄마다 새로 이터레이터를 반환하는 함수를 받는 것이다. # In[12]: def normalize_func(get_iter): total = sum(get_iter()) result = [] for value in get_iter(): percent = 100 * value / total result.append(percent) return result # In[13]: path = 'my_numbers.txt' percentages = normalize_func(lambda: read_visits(path)) print(percentages) # 하지만 람다함수를 넘기는 것은 보기에 좋지 않다. # # 같은 결과를 달성하는 더 나은 방법은 이터레이터 프로토콜을 구현한 새로운 컨테이너 클래스를 제공하는 것이다. # # 이터레이터 프로토콜은 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차다. # In[14]: 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) # In[15]: visits = ReadVisits(path) percentages = normalize(visits) print(percentages) # 이 코드가 정삭 작동 하는 이유는 normalizer 함수 안의 sum 메서드와 for 루프가 __iter__ 를 호출해서 각각 객체를 만들기 때문이다. # # 이 접근법의 유일한 단점은 입력 데이터를 여러번 읽는다는 것이다. # # 아래는 반복적으로 이터레이션할 수 없는 인자인 경우에는 TypeError를 발생시켜줄 수 있다. # In[16]: 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 # In[17]: normalize_defensive([1,2,3]) # 다른 대안으로 collections.abc 내장 모듈은 isinstance를 사용해 잠재적인 문제를 검사할 수 있는 Iterator 클래스를 제공한다. # In[19]: from collections.abc import Iterator # In[20]: 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 # In[22]: visits = [15, 35, 80] percentages = normalize_defensive(visits) print(percentages) # In[23]: visits = ReadVisits(path) percentages = normalize_defensive(visits) print(percentages) # In[24]: visits = [15, 35, 80] it = iter(visits) percentages = normalize_defensive(it) # ## 기억해야 할 내용 # - 입력 인자를 여러 번 이터레이션하는 함수나 메서드를 조심하라. 입력받은 인자가 이터레이터면 함수가 이상하게 작동하거나 결과가 없을 수 있다. # - 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 iter, next 내장 함수나 for 루프 등의 관련 식과 상호작용하는 절차를 정의한다. # - __iter__ 메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다. # - 어떤 값이 이터레이터인지 감지하려면, 이 값을 iter 내장 함수에 넘겨서 반환되는 값이 원래 값과 같은지 확인하면 된다. 다른 방법으로 collections.abc.Iterator 클래스를 isintance와 함께 사용할 수 있다.