파이썬 코딩의 기술 - 31
인자에 대해 이터레이션할 때는 방어적이 돼라
객체가 원소로 들어 있는 리스트를 함수가 파라미터로 받았을 때, 이 리스트를 여러 번 이터레이션하는 것이 중요할 때가 종종 있다.
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)
assert sum(percentages) == 100.0
# >>> [11.53, 26.92, 61.53]
이 코드의 규모 확장성을 높이려면 모든 도시에 대한 여행자 정보가 들어 있는 파일에서 데이터를 읽어야 한다.
파일을 읽는 함수를 재사용해야 할 수도 있으므로 제너레이터를 정의한다.
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
normalize
함수에 read_visits
가 반환한 값을 전달하면 아무 결과도 나오지 않는다.
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)
# >>> []
이런 현상이 일어난 이유는 이터레이터가 결과를 단 한 번만 만들어내기 떄문이다.
이미 StopIteration 예외가 발생한 이터레이터나 제너레이터를 다시 이터레이션하면 아무 결과도 얻을 수 없다.
it = read_visits('my_numbers.txt')
print(list(it))
print(list(it))
# >>> [15, 35, 80]
# >>> [] : 이미 모든 원소를 다 소진했다.
혼란스럽게도 이미 소진된 이터레이터에 대해 이터레이션을 수행해도 아무런 오류가 발생하지 않는다.
for 루프, 리스트 생성자, 그 외 파이썬 표준 라이브러리에 있는 많은 함수가 일반적인 연산 도중에 StopIteration 예외가 던져지는 것을 가정한다.
이런 함수들은 출력이 없는 이터레이터와 이미 소진돼버린 이터레이터를 구분할 수 없다.
이 문제를 해결하기 위해 입력 이터레이터를 명시적으로 소진시키고 이터레이터의 전체 내용을 리스트에 넣을 수 있다.
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
이 함수는 read_visits
제너레이터가 반환하는 값에 대해서도 잘 작동한다.
it = read_visits('my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
assert sum(percentages) == 100.0
# >>> [11.53, 26.92, 61.53]
이런 접근 방식의 문제점은 입력 이터레이터의 내용을 복사하면 메모리를 엄청나게 많이 사용할 수 있다는 것이다.
이 문제는 처음에 read_visits
를 제너레이터로 바꿔 쓰기로 결정했던 근본적인 이유인 규모 확장성 문제와 같다.
이 문제를 해결하는 방법은 호출될 때마다 새로 이터레이터를 반환하는 함수를 받는 것이다.
def normalize_func(get_iter):
total = sum(get_iter()) # 새 이터레이터
result = []
for value in get_iter():
percent = 100 * value / total
result.append(percent)
return result
normalize_func
를 사용할 때, 매번 제너레이터를 호출해서 새 이터레이터를 만들어내는 lambda
식을 전달할 수 있다.
path = 'my_numbers.txt'
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
assert sum(percentages) == 100.0
# >>> [11.53, 26.92, 61.53]
작동하기는 하지만, 이렇게 람다 함수를 넘기는 것은 보기에 좋지 않다.
같은 결과를 달성하는 더 나은 방법은 이터레이터 프로토콜(iterator protocol)을 구현한 새로운 컨테이너 클래스를 제공하는 것이다.
이터레이터 프로토콜은 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차다.
파이썬에서 for x in foo
와 같은 구문을 사용하면, 실제로는 iter(foo)
를 호출 한다.
iter
내장 함수는 foo.__iter__
라는 특별 메서드를 호출한다.
__iter__
메서드는 반드시 이터레이터 객체(이 객체는 __next__
특별 메서드를 정의해야 한다)를 반환해야 한다.
for
루프는 반환받는 이터레이터 객체가 데이터를 소진(이터레이터는 StopIteration 예외를 던진다)할 때까지 반복적으로 이터레이터 객체에 대해 __next__
내장 함수를 호출한다.
실제로 코드를 작성할 때 정의하는 클래스에서 __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)
이 새로운 컨테이너 타입을 원래의 normalize
함수에 넘기면 코드를 전혀 바꾸지 않아도 함수가 잘 작동ㅎ나다.
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
assert sum(percentages) == 100.0
# >>> [11.53, 26.92, 61.53]
이 코드가 잘 작동하는 이유는 normalize
함수 안의 sum
메서드가 ReadVisits.__iter__
를 호출해서 새로운 이터레이터 객체를 할당하기 때문이다.
각 숫자를 정규화하기 위한 for
루프도 __iter__
를 호출해서 두 번째 이터레이터 객체를 만든다.
두 이터레이터는 서로 독립적으로 진행되고 소진된다.
이 접근 방법의 유일한 단점은 입력 데이터를 여러 번 읽는다는 것이다.
프로토콜에 따르면, 이터레이터가 iter
내장 함수에 전달 되는 경우에는 전달받은 이터레이터가 그대로 반환된다.
반대로 컨테이너 타입이 iter
에 전달되면 매번 새로운 이터레이터 객체가 반환된다.
따라서 입력값이 이런 동작을 하는지 검사해서 반복적으로 이터레이션 할 수 없는 인자인 경우에는 TypeError를 발생시켜서 인자를 거부할 수 있다.
def normalize_defensive(numbers):
if iter(numbers) is numbers: # 이터레이터 X
raise TypeError('컨테이너를 제공해야 합니다')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
다른 대안으로 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
이런 식으로 컨테이너를 사용하는 방법은 앞의 normalize_copy
함수처럼 전체 입력 이터레이터를 복사하고 싶지 않을 때 유용하지만, 입력 데이터를 여러 번 이터레이션해야 한다.
normalize_defensive
함수는 리스트와 ReadVisits
에 대해 모두 제대로 작동한다.
리스트나 ReadVisits
모두 이터레이터 프로토콜을 따르는 이터러블 컨테이너이기 때문이다.
이 함수는 입력이 컨테이너가 아닌 이터레이터면 예외를 발생시킨다.
visits = [15, 35, 80]
it = iter(visits)
normalize_defensive(it)
# >>> TypeError!
비동기 이터레이터에 대해서도 같은 접근 방식을 사용할 수 있다.
기억해야 할 내용
- 입력 인자를 여러 번 이터레이션하는 함수나 메서드를 조심하라. 입력받은 인자가 이터레이터면 함수가 이상하게 작동하거나 결과가 없을 수 있다.
- 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가
iter
,next
내장 함수나for
루프 등의 관련 식과 상호작용하는 절차를 정의한다.__iter__
메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다.- 어떤 값이 이터레이터인지 감지하려면, 이 값을
iter
내장 함수에 넘겨서 반환되는 값이 원래 값과 같은지 확인하면 된다. 다른 방법으로collections.abc.Iterator
클래스를 isinstance와 함께 사용할 수도 있다.
Author And Source
이 문제에 관하여(파이썬 코딩의 기술 - 31), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@primadonna/파이썬-코딩의-기술-31저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)