인자에 대한 이터레이션을 할 때는 방어적으로 해라
20 Jan 2022
[Effective Python] Better way 31: 인자에 대한 이터레이션을 할 때는 방어적으로 해라
- 입력 인자를 여러번 이터레이션하는 함수나 메서드를 조심해라. 입력받은 인자가 이터레이터이면 함수가 이상하게 작동하거나 결과가 없을 수 있다.
- 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 iter, next 내장 함수나 for 루프등의 관련식과 상호작용하는 절차를 정의한다.
__iter__
메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다.- 어떤 값이 컨테이너가 아닌 이터레이터인지 알려면, 이 값을 iter 내장함수에 넘겨서 반환되는 값이 원래 값과 같은지 확인하면 된다. 다른 방법으로 collection.abc, Iterator 클래스를 isinstance와 함께 사용할 수 있다.
- Better way 31에서 말하려는 바는 객체가 원소로 들어 있는 리스트를 함수의 파라미터로 받았을때, 이 리스트를 여러번 iteration할 경우에 Iterator protocol을 사용하여 가독성 좋은 코드를 작성하라는 것이다. Iterator protocol을 사용하기 전에 적합하지 않은 방법 몇가지를 소개하고자 한다.
-
첫번째 방법 단순히 generator가 iterator를 반환한것을 재사용 하려고 하는 경우이다.
def normalize(numbers): total = sum(numbers) print(total) result = [] for value in numbers: precent = 100 * value / total result.append(precent) return result def read_visits(data_path): with open(data_path) as f: for line in f: yield int(line) it = read_visits('my_numbers.txt') perceptages = normalize(it) print(perceptages) print(list(it)) # [] # []
이렇게 빈 리스트만 출력하는 이유는 generator로부터 받은 iterator가 sum()함수에 들어간 이후에 StopIteration 예외가 발생했기 때문이다. (하지만 오류는 발생하지 않는다.) For loop, list 생성자, 또는 loop가 돌아가는 모든 함수에서 StopIteration 예외를 던진다. 출력이 없는 함수의 경우 StopIteration예외가 던져지는 것을 인식할 수 없다.
-
두번째 방법은 함수안에 iterator를 복사하여 사용하는 것이다.
def normalize(numbers): numbers_copy = list(numbers) total = sum(numbers_copy) result = [] for value in numbers_copy: precent = 100 * value / total result.append(precent) return result print(perceptages) print(list(it)) # [11.538461538461538, 26.923076923076923, 61.53846153846154] # []
이런 방식의 문제점은 iterator의 복사로 인해서 메모리를 많이 사용할 수 있다는 것이다. 하지만 이런 방식이면 generator를 read_visit함수에서 쓸 필요가 없다.
-
세번째 방법은 매번 람다함수를 호출하여 iterator를 반환 받는 것이다.
def normalize(getiter): total = sum(getiter()) result = [] for value in getiter(): precent = 100 * value / total result.append(precent) return result it = lambda: read_visits('my_numbers.txt') perceptages = normalize(it) print(perceptages) print(list(it())) # [11.538461538461538, 26.923076923076923, 61.53846153846154] # [15, 35, 80]
하지만 이 방법은 가독성이 좋지 못하다.
-
이제 Iterator protocol에 대해 알아보자.
for x in foo
위와 같은 구문을 사용하면 실제로는
iter(foo)
를 호출한다. 그러면 iter 내장 함수는foo.__iter__
이라는 특별 메서드를 호출한다.__iter__
메서드는 반드시 iterator 객체를 반환해야 한다. for 루프는 반환 받은 객체가 StopIteration 예외를 던질 때까지 next 내장함수를 호출한다. (iterator 객체는 반드시__next__
특별 메서드를 정의 해야 한다.)def normalize(getiter): total = sum(getiter()) result = [] for value in getiter(): precent = 100 * value / total result.append(precent) return result class ReadVisits: def __init__(self,datapath:str='my_numbers.txt'): self.datapath = datapath def __iter__(self): with open(self.datapath) as f: for line in f: yield int(line) it = ReadVisits perceptages = normalize(it) print(perceptages) print(list(it()))
sum(), for 루프에서
it.__iter__
를 호출항여 새로운 이터레이터 객체를 할당 받는다. 이때, 전달 받은 인자가 iterator인지 확인하기 위해iter
내장 함수를 사용할 수 있다.if iter(getiter()) is getiter(): raise TypeError
iter
는 Iterator를 인자로 받으면 그대로 반환되고, container 타입을 받응면 새로운 iterator 객체가 반환된다. 다른 방법으로는collections.abc
내장 모듈isinstance
를 확인할 수 있다. asyncio iterator에 대해서도 같은 접근 방식을 사용할 수 있다.