[Python] Skill of coding - 인수를 순회할 때는 방어적으로

Hyeseong·2020년 12월 9일
0

python skill of coding

목록 보기
10/18

인수를 순회할 떄는 방어적으로 하자

파라미터로 객체의 리스트를 받는 함수에서 리스트를 여러 번 순회해야 할 때가 있다. 특정 도시의 여행자 수를 분석하고 싶은 경우, 데이터 집합은 각 도시의 방문자 수라고 하자. 각 도시에서 전체 여행자 중 몇 퍼센트를 받아들이는지 알고 싶을 것이다.

이런 작업을 하려면 정규화 함수가 필요하다. 정규화 함수에서는 입력을 합산해서 연도별 총 여행자수를 구합니다. 그러고 나서 각 도시의 방문자 수를 전체 방문자 수로 나누어 각 도시가 전체에서 차지하는 비중을 알아냅니다.

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


# Example 2
visits = [15, 35, 80]
percentages = normalize(visits)
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]

방문 리스트를 확대하려면 해당 특정 도,시,군 단위의 모든 행정 자치구역의 인구가 들어 있는 파일에서 데이터를 읽어야해요. 이 작업을 수행하는 제너레이터를 정의할텐데, 그러면 나중에 같은 함수를 재사용하여 더 큰 데이터 세트인 전 세계의 여행자 수를 계산할 수 있기 때문이에요.

path = 'my_numbers.txt'
with open(path, 'w') as f:
    for i in (15, 35, 80):
        f.write('%d\n' % i)

def read_visits(data_path):
    with open(data_path) as f:
        for line in f:
            yield int(line)

놀랍게도 제너레이터의 반환 값에 normalize를 호출하면 아무 결과도 생성되지 않아요.

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 루프와 list생성자, 파이썬 표준 라이브러리의 많은 함수는 정상적인 동작 과정에서 StopIteration 예외가 일어날 것이라고 기대해요. 이런 함수는 결과가 없는 이터레이터와 결과가 있었지만 이미 소진한 이터레이터의 차이를 알려주지 않아요.

이 문제를 해결하려면 입력 이터레이터를 명시적으로 소진하고 전체 콘첸츠의 복사본을 리스트에 저장해야 해요. 그러고 나면 리스트 버전의 데이터를 필요한 만큼 순회할 수 있어요. 다음은 이전과 동일하지만 입력 이터레이터를 방어적으로 복사하는 함수에요.

it = read_visits('my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]

이 방법의 문제는 입력받은 이터레이터 콘텐츠의 복사본이 클 수도 있다는 점이에요 이런 이터레이터를 복사하면 프로그램의 메모리가 고갈되어 동작을 멈출 수도 있어요. 이 문제를 피하는 한 가지 방법은 호출될 때마다 새 이터레이터를 반환하는 함수를 받게 만드는 것이에요.

def normalize_func(get_iter):
    total = sum(get_iter())   # New iterator
    result = []
    for value in get_iter():  # New iterator
        percent = 100 * value / total
        result.append(percent)
    return result

path = 'my_numbers.txt'
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]

normalize_func를 사용하려면 제너레이터를 호출해서 매번 새 이터레이터를 생성하는 람다 표현식을 넘겨주면 되요.

코드가 잘 동작하긴 하지만, 이렇게 람담 함수를 넘겨주는 방법은 투박해요. 같은 결과를 얻는 더 좋은 방법은 이터레이터 프로토콜(iterator protocol)을 구현하여 새 컨테이너 클래스를 제공하는 것이에요.

이터레이터 프로토콜은 파이썬의 for루프와 관련 표현식이 컨테이너 타입의 콘텐츠를 탐색하는 방법을 나타내요.

파이썬은 for x in foo 같은 문장을 만나면 실제로는 iter(foo)를 호출해요. 그러면 내장함수 iter는 매직메소드 foo.__iter__를 호출하고요. __iter__메서드는 (__next__라는 특별한 메서드를 구현하는) 이터레이터 객체를 반환해야해요. 마지막으로 for루프는 이터레이터를 모두 소진할 때까지 (그래서 StopIteration 예외가 발생할 때까지) 이터레이터 객체에 내장 함수 next를 계속 호출해요.

복잡해 보이지만 사실 클래스의 __iter__ 메서드를 제너레이터로 구현하면 이렇게 동작하게 만들 수 있어요. 다음은 여행자 데이터를 담은 파일을 읽는 이터러블 컨테이너 클래스에요.


visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
[11.538461538461538, 26.923076923076923, 61.53846153846154]

새로 정의한 컨테이너 타입은 원래의 함수에 수정을 가하지 않고 넘겨도 제대로 동작합니다.

이 코드가 동작하는 이유는 normalize의 sum메서드가 새 이터레이터 객체를 할당하려고 ReadVisits.__iter__를 호출하기 때문이에요. 숫자를 정규화하는 for 루프도 두 번째 이터레이터 객체를 할당할 때 __iter__를 호출한다. 두 이터레이터는 독립적으로 동작하므로 각각의 순회 과정에서 모든 입력 데이터 값을 얻을 수 있어요. 이방법의 유일한 단점은 입력 데이터를 여러 번 읽는 다는 점이에요.

이제 ReadVisits와 같은 컨테이너가 어떻게 동작하는지 알았으니 파라미터가 단순한 이터레이터가 아님을 보장하는 함수를 작성할 차례에요.

프로토콜에 따르면 내장 함수 iter에 이터레이터를 넘기면 이터레이터 자체가 반환해요. 반면에 iter에 컨테이너 타입을 넘기면 매번 새 이터레이터 객체가 반환되요. 따라서 이 동작으로 입력값을 테스트해서 이터레이터면 TypeError를 일으켜서 거부하게 만들면 되요.


visits = [15, 35, 80]
normalize_defensive(visits)  # No error
visits = ReadVisits(path)
normalize_defensive(visits)  # No error
[11.538461538461538, 26.923076923076923, 61.53846153846154]

normalize_defensive는 normalize_defensive처럼 입력 이터레이터 전체를 복사하고 싶지 않지만, 입력 데이터를 여러 번 순회해야 할 때 사용하면 좋아요. 이 함수는 list와 ReadVisits를 입력으로 받으면 입력이 컨테이너이므로 기대한 대로 동작해요. 이터레이터 프로토콜을 따르는 어떤 컨테이너 타입에 대해서도 제대로 동작 할거에요.

it = iter(visits)
normalize_defensive(it)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-67-7655d24993e5> in <module>
      1 it = iter(visits)
----> 2 normalize_defensive(it)

<ipython-input-65-8e642fad41da> in normalize_defensive(numbers)
      1 def normalize_defensive(numbers):
      2     if iter(numbers) is iter(numbers):  # An iterator -- bad!
----> 3         raise TypeError('Must supply a container')
      4     total = sum(numbers)
      5     result = []

TypeError: Must supply a container

핵심정리

입력 인수를 여러 번 순회하는 함수를 작성할 때 주의하자. 입력 인수가 이터레이터라면 이상하게 동작해서 값을 잃어버릴 수있다.
파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 내장함수 iter, next와 for for루프 및 관련 표현시고가 상호 작용하는 방법을 정의한다.
__iter__ 메서드를 제너레이터로 구현하면 자신만의 이터러블 컨테이너 타입을 쉽게 정의할 수 있다.
어떤 값이 iter를 두 번 호출했을 때 같은 결과가 나오고 내장 함수 next로 전진ㅅ시킬 수 있다면 그 값은 컨테이너가 아니 이터레이터다.

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글