defnormalize(numbers):total=sum(numbers)# 이터레이터 한 번 소모result=[]forvalueinnumbers:# 이미 순회가 끝난 이터레이터는 순회를 해도 결과가 나오지 않음percent=100*value/totalresult.append(percent)returnresultdefread_visits(data_path):withopen(data_path)asf:forlineinf:yieldint(line)it=read_visits("/tmp/my_numbers.txt")percentages=normalize(it)print(percentages)>>>[]
위의 예제에서 제너레이터의 반환 값에 normalize를 호출하면 아무 결과도 생성되지 않음
이터레이터가 결과를 한 번만 생성하기 때문
이미 StopIteration 예외를 일으킨 이터레이터나 제너레이터를 순회하면 어떤 결과도 얻을 수 없음
it=read_visits("/tmp/my_numbers.txt")print(list(it))print(list(it))# 이미 소진함>>>[15,35,80][]
이미 소진한 이터레이터를 순회하더라도 오류가 일어나지는 않음
많은 함수들이 결과가 없는 이터레이터와 결과가 있었지만 이미 소진한 이터레이터의 차이를 알려주지 않음
defnormalize_func(get_iter):total=sum(get_iter())# 새 이터레이터result=[]forvalueinget_iter():percent=100*value/totalresult.append(percent)returnresultpercentages=normalize_func(lambda:read_visits(path))# 제너레이터를 호출하여 매번 새 이터레이터를 생성하는 람다 표현식을 넘겨줌print(percentages)>>>[11.538461538461538,26.923076923076923,61.53846153846154]
위의 예제도 잘 돌아가기는 하지만 람다 함수를 넘겨주는 방법은 세련되지 못함
같은 결과를 얻을 수 있는 더 좋은 방법: 이터레이터 프로토콜을 구현한 새 컨테이너 클래스를 제공하는 것
이터레이터 프로토콜
for 루프와 관련 표현식이 컨테이너 타입의 콘텐츠를 탐색하는 방법
파이썬은 for x in foo같은 문장을 만나면 실제로는 iter(foo)를 호출
iter() 함수: iterable하지만 이터레이터는 아닌 list를 listiterator 타입으로 변경하는 함수
iter(foo)를 호출하면 특별한 메서드인 foo.__iter__를 호출
__iter__ 메서드: (__next__라는 특별한 메서드를 구현하는) 이터레이터를 반환
마지막으로 for 루프는 이터레이터를 모두 소진할 때까지 (StopIteration 예외를 일으킬 때까지) 이터레이터 객체의 내장 함수 next를 계속 호출
결국 __iter__ 메서드를 제너레이터로 구현하면 이렇게 동작하게 구현이 가능
다음은 여행자 데이터를 담은 파일을 읽는 이터러블 컨테이너 클래스
classReadVisits(object):# 이 클래스의 구현 방식이 이터레이터 프로토콜def__init__(self,data_path):self.data_path=data_pathdef__iter__(self):withopen(self.data_path)asf:forlineinf:yieldint(line)defnormalize(numbers):# 원래 함수에 수정을 가하지 않고 넘겨도 제대로 동작total=sum(numbers)result=[]forvalueinnumbers:percent=100*value/totalresult.append(percent)returnresultpath="/tmp/my_numbers.txt"visits=ReadVisits(path)percentages=normalize(visits)print(percentages)>>>[11.538461538461538,26.923076923076923,61.53846153846154]
이 코드가 동작하는 이유:
normalize의 sum 메서드가 새 이터레이터 객체를 할당하려고 ReadVisits.__iter__를 호출하기 때문
숫자를 정규화하는 for 루프도 두 번째 이터레이터 객체를 할당할 때 __iter__를 호출함
두 이터레이터는 독립적으로 동작: 각각의 순회 과정에서 모든 입력 데이터 값을 얻을 수 있음
유일한 단점: 입력 데이터를 여러 번 읽는다는 점
위의 예제에 파라미터가 단순한 이터레이터가 아님을 보장하는 함수를 작성해야 함
내장함수 iter에 이터레이터를 넘기면: 이터레이터 자체 반환
내장함수 iter에 컨테이너 타입을 넘기면: 매번 새 이터레이터 객체가 반환
defnormalize_defensive(numbers):ifiter(number)isiter(numbers):# 이터레이터는 거부하는 조건raiseTypeError("Must supply a container")total=sum(numbers)result=[]forvalueinnumbers:percent=100*value/totalresult.append(percent)returnresultpath="/tmp/my_numbers.txt"visits=[15,35,80]normalize_defensive(visits)# 오류 없음visits=ReadVisits(path)normalize_defensive(visits)# 오류 없음it=iter(visits)normalize_defensive(it)# 오류 발생: iterable하더라도 컨테이너가 아니기 때문
Comments