betterway31이터레이션 방어

김승환·2021년 7월 11일

코딩의 기술

목록 보기
21/36

인자에 대해 이터레이션할 때는 방어적이 돼라

  • 객체가 원소로 들어 있는 리스트를 함수가 파라미터로 받았을 때, 이 리스트를 여러 번 이터레이션하는 것이 중요할 때가 있다.
  • 예를 들어 미국 텍사스 주의 여행자 수를 분석하고 싶다고 하자. 데이터 집합이 도시별 방문자 수라고 가정하자. 이때 각 도시가 전체 여행자 수 중에서 차지하는 비율을 계산하고 싶다.
# ex) 1년간 전체 여행자 수를 계산하기 위해 입력 전체의 합계를 내고 이 합계로 각 도시의 반문자 수를 나누는 정규화 함수가 필요
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.538461538461538, 26.923076923076923, 61.53846153846154]

  • 위 코드의 규모 확장성을 높이려면 텍사스의 모든 도시에 대한 여행자 정보가 들어 있는 파일에서 데이터를 읽어야 한다.
  • 나중에 전 세계를 대상으로 여행자 분석을 실시할 때 파일을 읽는 함수를 재사용해야 할 수도 있으므로 제너레이터를 정의한다.
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)

[]

  • 위 현상이 일어나는 이유는 이터레이터가 결과를 단 한 번만 만들어내기 때문
  • 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
it = read_visits('my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
assert sum(percentages) == 100.0

[11.538461538461538, 26.923076923076923, 61.53846153846154]

위 방식의 문제점

  • 이터레이터의 내용을 복사하면 메모리를 엄청나게 많이 사용할 수 있다.
  • 이터레이터를 복사하는 과정에서 메모리 부족으로 인해 프로그램 중단될 수 있다.
  • 이 문제는 처음 read_visits를 제너레이터로 바꿔 쓰기로 결정했던 근본적인 이유인 규모 확장성 문제와 같다.

해결방법

  • 호출될 때마다 새로 이터레이터를 반환하는 함수를 받는 것
  • normalize_func를 사용할 때, 매번 제너레이터를 호출해서 새 이터레이터를 만들어내는 lambda식을 전달 할 수 있다.
def normalize_func(get_iter):
    total = sum(get_iter())  # 새 이터레이터
    result = []
    for value in get_iter(): # 새 이터레이터
        percent = 100 * value / total
        result.append(percent)
    return result

def read_visits(data_path):
    with open(data_path) as f:
        for line in f:
            yield int(line)
#
path = 'my_numbers.txt'
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
assert sum(percentages) == 100.0

[11.538461538461538, 26.923076923076923, 61.53846153846154]

  • 작동은 하지만 람다 함수를 넘기는 것은 보기에 좋지않다.
  • 같은 결과를 달성하는 더 나은 방법은 이터레이터 프로토콜을 구현한 새로운 컨테이너 클래스를 제공하는 것

이터레이터 프로토콜

  • 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차
  • for x in foo. iter 라는 특별 메서드를 호출한다.
  • iter 메서드는 반드시 이터에리터 객체를 반환해야 한다.( next 특별 메서드 정의)
  • for 루프는 반환받는 이터레이터 객체가 데이터를 소진할 때까지 반복적으로 이터레이터 객체에 대해 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)

def normalize(numbers):
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result

            
#새로운 컨데이터 타입을 원래의 normalize 함수에 넘기면 코드를 전혀 바꾸지 않아도 함수가 잘 작동한다.
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
assert sum(percentages) == 100.0

[11.538461538461538, 26.923076923076923, 61.53846153846154]

  • normalize 함수 안의 sum 메서드가 ReadVisits.iter를 호출해서 새로운 이터레이터 객체를 할당한다.
  • 두 이터레이터는 서로 독립 진행
  • 단점은 입력 데이터를 여러 번 읽는다.
  • ReadVisits같은 컨테이너가 어떻게 동작하는지 안다면 파라미터로 받은 값이 단순* 한 이터레이터가 아니라도 잘 작동하는 함수나 메서드를 작성할 수 있다.
  • 프로토콜에 따르면, 이터레이터가 iter 내장 함수에 전달 되는 경우에는 전달 받은 이터레이터가 그대로 반환된다.
  • 반대로 컨테이너 타입이 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

다른 대안으로 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)
  • 컨테이너를 사용하는 방법은 앞의 normalize_copy 함수처럼 전체 입력 이터레이터를 복사하고 싶지 않을 때 유용하지만, 입력 데이터를 여러 번 이터레이션 해야한다.
  • 이 함수(normalize_defensive)는 리스트와 ReadVisits에 대해 모두 제대로 작동한다.
  • 리스트나 ReadVisits 모두 이터레이터 프로토콜을 따르는 이터러블 컨테이너이기 때문이다.
print(percentages)
assert sum(percentages) == 100.0

visits = ReadVisits(path)
percentages = normalize_defensive(visits)
assert sum(percentages) == 100.0

[11.538461538461538, 26.923076923076923, 61.53846153846154]
NameError Traceback (most recent call last)
in
2 assert sum(percentages) == 100.0
3
----> 4 visits = ReadVisits(path)
5 percentages = normalize_defensive(visits)
6 assert sum(percentages) == 100.0

NameError: name 'ReadVisits' is not defined

visits = [15, 35, 80]
it = iter(visits)
# 오류가 나는 부분. 오류를 보고 싶으면 커멘트를 해제할것
normalize_defensive(it)

TypeError Traceback (most recent call last)
in
2 it = iter(visits)
3 # 오류가 나는 부분. 오류를 보고 싶으면 커멘트를 해제할것
----> 4 normalize_defensive(it)

in normalize_defensive(numbers)
3 def normalize_defensive(numbers):
4 if isinstance(numbers, Iterator): # 반복 가능한 이터레이터인지 검사하는 다른 방법
----> 5 raise TypeError('컨테이너를 제공해야 합니다')
6 total = sum(numbers)
7 result = []

TypeError: 컨테이너를 제공해야 합니다

profile
인공지능 파이팅!

0개의 댓글