NLP Blog

이펙티브 파이썬 - 21. 키워드 전용 인수로 명료성을 강요하자

|

BetterWay21. 키워드 전용 인수로 명료성을 강요하자

  • 키워드로 인수를 넘기는 방법은 파이썬 함수의 강력한 기능임
  • 키워드 인수의 유연성 덕분에 쓰임새가 분명하게 코드 작성 가능
  • ex) 어떤 숫자를 다른 숫자로 나눈다고 해보자. 하지만 특별한 경우를 매우 주의해야함
    • 때로는 ZeroDivisionError 예외를 무시하고 무한대 값을 반환하고 싶을 수 있음
    • OverflowError 예외를 무시하고 0을 반환하고 싶을 수도 있음
def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
  • 이 함수를 사용하는 방법은 간단데스
    • 나눗셈에서 일어나는 float 오버플로우를 무시하고 0을 반환
result = safe_division(1, 10**500, True, False)
print(result)
0.0
  • 다음 함수 호출은 0으로 나누면서 일어나는 오류를 무시하고 무한대 값을 반환
result = safe_division(1, 0, False, True)
print(result)
inf
  • 문제는 예외 무시 동작을 제어하는 두 bool 인수의 위치를 혼동하기 쉽다는 점이다.
  • 이것 때문에 찾기 어려운 버그가 쉽게 발생할 수 있다.
  • 이런 코드의 가독성을 높이는 한 가지 방법은 키워드 인수를 사용하는 것
  • 다음과 같이 함수가 기본적으로 매우 주의 깊고 항상 예외를 다시 일으키게 만들 수 있다.
def safe_division_b(number, divisor, ignore_overflow=False, ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
  • 이렇게 하면 호출하는 쪽에서 키워드 인수로 특정 연산에는 기본 동작을 덮어쓰고 무시할 플래그를 지정할 수 있다.
print(safe_division_b(1, 10**500, ignore_overflow=True))
print(safe_division_b(1, 0, ignore_zero_division=True))
0.0
inf
  • 문제는 이런 키워드 인수가 선택적인 동작이라서 함수를 호출하는 쪽에 키워드 인수로 의도를 명확하게 드러내라고 강요할 방법이 없다는 점이다.
  • safe_division_b라는 새 함수를 정의해도 여전히 위치 인수를 사용하는 이전 방식으로 호출 할 수 있다.
safe_division_b(1, 10**500, True, False)
0.0
  • 이처럼 복잡한 함수를 작성할 때는 호출하는 쪽에서 의도를 명확히 드러내도록 요구하는 게 낫다.
  • 파이썬3에서는 키워드 전용 인수(keyword-only argument)로 함수를 정의해서 의도를 명확히 드러내도록 요구할 수 있다.
  • 키워드 전용 인수는 키워드로만 넘길 뿐, 위치로는 절대 넘길 수 없다.

  • 다음은 키워드 전용 인수로 safe_division 함수를 다시 정의한 버전이다.
  • 인수 리스트에 있는 * 기호는 위치 인수의 끝과 전용 인수의 시작을 가리킨다.
def safe_division_c(number, divisor, *, ignore_overflow=False, ignore_zero_division=True):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
# 이제 키워드 인수가 아닌 위치 인수를 사용하는 함수 호출은 동작하지 않는다.
safe_division_c(1, 10**500, True, False)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-8-f48f06e28880> in <module>()
      1 # 이제 키워드 인수가 아닌 위치 인수를 사용하는 함수 호출은 동작하지 않는다.
----> 2 safe_division_c(1, 10**500, True, False)


TypeError: safe_division_c() takes 2 positional arguments but 4 were given
# 키워드 인수과 그 기본값은 기대한 대로 동작한다.
safe_division_c(1, 0, ignore_zero_division=True)  # 문제 없음

try:
    safe_division_c(1, 0)
except ZeroDivisionError:
    pass

파이썬 2의 키워드 전용 인수

  • 불행하게도 파이썬 2에서는 * 같은 명시적 문법이 없음
  • 하지만 인수 리스트에 ** 연산자를 사용해 올바르지 않은 함수 호출을 할 때 TypeError를 일으키는 방법으로 같은 동작을 만들 수 있다.
  • 가변 개수의 위치 인수 대신에 키워드 인수를 몇 개든(심지어 정의하지 않았을 때 조차도) 받을 수 있다는 점만 빼면 ** 연산자는 * 연산자와 비슷하다.
# 파이썬2
def print_args(*args, **kwargs):
    print 'Positional:', args
    print 'Keyword: ', kwargs

print_args(1, 2, foo='bar', stuff='meep')
  • 파이썬 2에서는 safe_division**kwargs를 받게 만들어서 키워드 전용 인수를 받게 한다.
  • 그런 다음 pop 메서드로 kwargs 딕셔너리에서 원하는 키워드 인수를 꺼낸다.
  • 키가 없을 때의 기본값은 pop 메서드의 두번째 인수로 지정한다.
  • 마지마기으로 kwargs에 더는 남아 있는 키워드가 없음을 확인하여 호출하는 쪽에서 올바르지 않은 인수를 넘기지 않게 한다.
# 파이썬 2
def safe_division_d(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_div = kwargs.pop('ignore_zero_division:', False)
    if kwargs:
        raise TypeError('Unexpected **kwargs: %r' % kwargs)
    # ...

핵심 정리

  • 키워드 인수는 함수 호출의 의도를 더 명확하게 해준다
  • 특히 불 플래그를 여러 개 받는 함수처럼 헷갈리기 쉬운 함수를 호출할 때 키워드 인수를 넘기게 하려면 키워드 전용 인수를 사용하자.
  • 파이썬 3는 함수의 키워드 전용 인수 문법을 명시적으로 지원한다.
  • 파이썬 2에서는 **kwargs를 사용하고 TypeError 예외를 직접 일으키는 방법으로 함수의 키워드 전용 인수를 흉내낼 수 있다.

Comment  Read more

이펙티브 파이썬 - 20. 동적 기본 인수를 지정하려면 None과 docstring을 사용하자

|

BetterWay20. 동적 기본 인수를 지정하려면 None과 docstring을 사용하자

  • 키워드 인수의 기본값으로 비정적(non-static)타입을 사용해야 할 때도 있음
    • ex) 이벤트 발생 시각까지 포함해 로깅 메시지를 출력한다고 하자.
    • 함수를 호출한 시각을 메시지에 포함
    • 함수가 호출될 때마다 기본 인수를 평가한다고 가정하고 다음과 같이 처리하려 할 것이다
from datetime import datetime
from time import sleep

def log(message, when=datetime.now()):
    print('%s: %s' % (when, message))

log('Hi there!')
sleep(0.1)
log('Hi again!')
2018-09-26 23:23:56.775506: Hi there!
2018-09-26 23:23:56.775506: Hi again!
  • datetime.now는 함수를 정의할 때 딱 한번만 실행되므로 타임스탬프가 동일하게 출력됨
  • 기본 인수의 값은 모듈이 로드될 때 한 번만 평가되며 보통 프로그램이 시작할 때 일어남
  • 이 코드를 담고 있는 모듈이 로드된 후에는 기본 인수인 datetime.now를 다시 평가하지 않음

  • 파이썬에서 결과가 기대한 대로 나오게 하려면 기본값은 None으로 설정하고 docstring(문서화 문자열) 으로 실제 동작을 문서화 하는게 관례
  • 코드에서 인수 값으로 None이 나타나면 알맞은 기본값을 할당하면 됨
def log(message, when=None):
    """
    Log a message with a timestamp

    Args:
        message: Message to print.
        when : datetime of when the message occurred.
            Defaults to the present time.
    """
    when = datetime.now() if when is None else when
    print('%s: %s' % (when, message))
log('Hi there')
sleep(0.1)
log('Hi again!')
2018-09-26 23:23:56.901623: Hi there
2018-09-26 23:23:57.005307: Hi again!

  • 기본 인수 값으로 None을 사용하는 바법은 인수가 수정 가능(mutable)할 때 특히 중요
  • ex) JSON 데이터로 인코드된 값을 로드한다고 해보자.
  • 데이터 디코딩이 실패하면 기본값으로 빈 딕셔너리를 반환하려고 한다. 다음을 보자
import json
def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default
  • 위 코드에는 datetime.now 예제와 같은 문제가 있음
  • 기본 인수 값은(모듈이 로드될 때) 딱 한 번만 평가되므로, 기본값으로 설정한 딕셔너리를 모든 decode 호출에서 공유, 이 문제는 예상치 못한 동작을 야기한다.
foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:',foo)
print('Bar:',bar)
Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}
  • foo와 bar가 같은 객체로 처리됨
  • 하나를 수정하면 다른 하나도 수정되고 있음
  • foo와 bar 둘 다 기본 파라미터가 같아서 생기는 문제
assert foo is bar
  • 키워드 인수의 기본값을 None으로 설정하고 함수의 docstring에 동작을 문서화해서 문제를 해결하자
def decode(data, default=None):
    """
    Load JSON data form a string.

    Args:
        data: JSON data to decode.
        defualt: Value to return if decoding fails.
            Defualts to an empty dictionary.
    """
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default
foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)
Foo: {'stuff': 5}
Bar: {'meep': 1}

핵심 정리

  • 기본 인수는 모듈 로드 시점에 함수 정의 과정에서 딱 한 번만 평가된다. 그래서 {}나 [] 같은 동적 값에는 이상하게 동작하는 원인이 되기도 한다.
  • 값이 동적인 키워드 인수에는 기본값으로 None을 사용하자. 그러고 나서 함수의 docstring에 실제 기본 동작을 문서화하자.

Comment  Read more

이펙티브 파이썬 - 19. 키워드 인수로 선택적인 동작을 제공하자

|

BETTER WAY 19. 키워드 인수로 선택적인 동작을 제공하자

  • 대부분의 다른 프로그래밍 언어와 마찬가지로 파이썬에서 함수를 호출할 때 인수를 위치로 전달할 수 있다.
def remainder(number, divisor):
    return number % divisor

assert remainder(20, 7) == 6
  • 파이썬 함수의 위치 인수를 모두 키워드로 전달할 수도 있다.
    • 이때 인수의 이름을 함수 호출의 괄호 안에 있는 할당문에서 사용
    • 필요한 위치 인수를 모두 지정한다면 키워드 인수로도 전달할 수 있다.
    • 키워드와 위치 인수를 섞어서 사용할 수 있다.
# 다음 호출은 모두 동일
remainder(20, 7)
remainder(20, divisor=7)
remainder(number=20, divisor=7)
remainder(divisor=7, number=20)
6
# 위치 인수는 키워드 인수 앞에 지정해야 한다.
remainder(number=20, 7)
  File "<ipython-input-3-bc33add96835>", line 2
    remainder(number=20, 7)
                        ^
SyntaxError: positional argument follows keyword argument
# 각 인수는 한 번만 지정할 수 있다.
remainder(20, number=7)
  • 키워드 인수의 유연성은 세 가지 중요한 이점이 있다.

  • 키워드 인수의 첫 번째 이점
    • 코드를 처음 보는 사람이 함수 호출을 더 명확하게 이해할 수 있다는 점
      • remainder 메서드의 구현을 보지 않고는 remainder(20, 7)호출에서 어떤 인수가 숫자이고 어떤 인수가 나눗수인지 분명하지 않음
      • 키워드 인수를 사용한 호출에서는 number=20, divisor=7을 보면 각각의 목적으로 어떤 파라미터를 사용했는지 명확하게 이해 가능

  • 키워드 인수의 두번째 이점
    • 함수를 정의할 때 기본값 설정 가능
      • 덕분에 함수에서 대부분은 기본값을 사용하지만 필요할 때 부가 기능을 제공할 수 있다.
      • 반복코드가 줄어들고 코드가 깔끔해짐
  • 예를 들어 큰 통에 들어가는 액체의 유속을 계산하고 싶다고해보자
  • 큰 통의 무게를 잴 수 있다면, 각기 다른 시각에 측정한 두 무게의 차이를 이용해 유속을 알 수 있음
def flow_rate(weight_diff, time_diff):
    return weight_diff / time_diff

weight_diff = 0.5
time_diff = 3
flow = flow_rate(weight_diff, time_diff)
print('%.3f kg per second' % flow)
  • 보통은 초당 킬로그램 단위로 유속을 아는 게 좋다. 하지만 센서의 최근 측정값을 이용해 시간이나 날짜처럼 더 큰 시간 단위로 계산하는게 좋을 때도 있다.
  • 함수의 인수에 기간 환산 계수를 추가하면 이런 동작 제공 가능
def flow_rate(weight_diff, time_diff, period):
    return (weight_diff/ time_diff) * period
  • 문제는 함수를 호출할 때마다, 심지어 초당 유속을 사용하는 일반적인 경우(period가 1일때)에도 period를 설정해야함
flow_per_second = flow_rate(weight_diff, time_diff, 1)
  • period 인수에 기본값을 설정하면 위의 코드를 좀 더 깔끔하게 만들 수 있다.
def flow_rate(weight_diff, time_diff, period=1):
    return (weight_diff / time_diff) * period
# period는 선택적임 인수
flow_per_second = flow_rate(weight_diff, time_diff)
flow_per_second = flow_rate(weight_diff, time_diff, period=3600)
  • 위의 코드는 간단한 기본값에는 잘 동작 (기본값이 복잡할 때는 다루기 까다롭다. better way 20에서 배울예정)

  • 키워드 인수의 세 번째 이점
    • 기존의 호출 코드와 호환성을 유지하면서도 함수의 파라미터를 확장할 수 있는 강력한 수단
      • 코드를 많이 수정하지 않고서도 추가적인 기능을 제공 가능
      • 버그가 생길 가능성을 줄일 수 있다.
  • 예를 들어 킬로그램 단위는 물론 다른 무게 단위로도 유속을 계산하려고 앞의 flow_rate 함수를 확장한다고 하자.
    • 원하는 측정 단위의 변환 비율을 새 선택 파라미터로 추가하여 확장
def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
    return ((weight_diff / units_per_kg) / time_diff) * period
  • units_per_kg 인수의 기본값은 1로, 반환되는 무게 단위는 킬로그램이 된다. 즉, 기존 코드의 호출 코드의 동작에는 변화가 없음을 의미
  • flow_rate를 새로 호출하는 코드에서는 새 키워드 인수로 새로운 동작을 지정할 수 있음
pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)
print("%.3f pounds per hour" % pounds_per_hour)
  • 위 방법의 유일한 문제는 period와 units_per_kg 같은 선택적인 키워드 인수를 여전히 위치 인수로도 넘길 수 있다는 점
pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2)
  • 선택적인 인수를 위치로 넘기면 3600과 2.2 값에 대응하는 인수가 무엇인지 명확하지 않아 혼종을 일으킬 수 있다.
    • 가장 좋은 방법은 항상 키워드 이름으로 선택적인 인수를 지정하고 위치 인수로는 아예 넘기지 않는 것

Note

  • 이런 선택적인 키워드 인수를 사용하면 *args를 인수로 받는 함수에서 하위 호환성을 지키기 어렵다. (better way 18)
  • 더 좋은 방법은 키워드 전용인수를 사용하는 것 (better way 21)

핵심 정리

  • 함수의 인수를 키워드로 지정할 수 있다.
  • 위치 인수만으로는 이해하기 어려울 때 키워드 인수를 쓰면 각 인수를 사용하는 목적이 명확해짐
  • 키워드 인수에 기본값을 지정하면 함수에 새 동작을 쉽게 추가할 수 있다. 특히, 함수를 호출하는 기존 코드가 있을 때 사용하면 좋다.
  • 선택적인 키워드 인수를 항상 위치가 아닌 키워드로 넘겨야 한다.

Comment  Read more

이펙티브 파이썬 - 18. 가변 위치 인수로 깔끔하게 보이게 하자

|

BETTER WAY 18. 가변 위치 인수로 깔끔하게 보이게 하자

  • 선택적인 위치 인수를 받게 만들면 함수 호출을 더 명확하게 할 수 있고 보기에 방해되는 요소를 없앨 수 있음
    • 이런 파라미터의 이름을 관례적으로 *args라고 해서 종종 ‘star args’라고도 한다
  • 예를 들어 디버그 정보 몇 개를 로그로 남긴다로 해보자.
    • 인수의 개수가 고정되어 있다면 메시지와 값 리스트를 받는 함수가 필요
def log(message, values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', [1, 2])
log('Hi there', [])
My numbers are: 1, 2
Hi there
  • 로그로 남길 값이 없을때 빈 리스트 []를 남겨야하는거 귀찮다
    • 두번째 인수를 아예 남겨둔다면 더 좋을 것
    • 파이썬에서는 * 기호를 바지막 위치 파라미터 이름 앖에 붙이면 된다.
  • 로그 메시지를 의미하는 첫번째 파라미터는 필수지만, 다음에 나오는 위치 인수는 몇 개든 선택적이다.
  • 함수 본문은 수정할 필요가 없고 호출하는 쪽만 수정해주면 된다.
def log(message, *values): # 유일하게 다른 부분
    if not values:
        print(message)
    else:
        values_str = ','.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', 1, 2)
log('Hi there')
My numbers are: 1,2
Hi there
  • 리스트를 log같은 가변 인수 함수를 호출하는데 사용하고 싶다면 * 연산자를 쓰면 된다.
    • 파이썬은 시퀀스에 들어 있는 아이템들을 위치 인수로 전달
favorites = [7, 33, 99]
log("Favorite colors", *favorites)

Favorite colors: 7,33,99

  • 가변 개수의 위치 인수를 받는 방법에는 두가지 문제가 있다.
    • 1) 가변 인수가 함수에 전달되기에 앞어 항상 튜플로 변환되는 점
      • 함수를 호출하는 쪽에서 제너레이터에 * 연산자를 쓰면 제너레이터가 모두 소진 될 때까지 순회됨을 의미
      • 결과로 만들어지는 튜플은 제너레이터로부터 생성된 모든 값을 담으므로 메모리를 많이 차지해 결국 프로그램이 망가지게 할 수도 있다.
def my_gernerator():
    for i in range(10):
        yield i

def my_func(*args):
    print(args)

it = my_gernerator()
my_func(*it)
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
  • *args를 받는 함수를 인수 리스트에 있는 입력의 수가 적당히 적다는 사실을 아는 상황에서 가장 좋은 방법
    • 이런 함수는 많은 리터럴이나 변수 이름을 한꺼번에 넘기는 함수 호출에 이상적, 주로 개발자들을 편하게 하고 코드의 가독성을 높이려고 사용

  • 2) 추후에 호풀 코드를 모두 변경하지 않고서는 새 위치 인수를 추가할 수 없다는 점
    • 인수 리스트의 앞쪽에 위치 인수를 추가하면 기존의 호출 코드가 수정없이는 이상하게 동작함
def log(sequence, message, *values):
    if not values:
        print('%s: %s' % (sequence, message))
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s: %s' % (sequence, message, values_str))


log(1, "Favorite", 7, 33) # 새로운 용법은 OK
log('Favorite number', 7, 33) # 오래된 용법은 제대로 동작하지 않음
1: Favorite: 7, 33
Favorite number: 7: 33
  • 이 코드의 문제점은 두 번째 호출이 sequence인수를 받지 못했기 때문에 7을 message 파라미터로 사용한다는 점
    • 이런 버그는 코드에서 예외를 일으키지 않고 계속 실행되므로 발견하기가 극히 어렵다.
    • 이런 문제가 생길 가능성을 완전히 없애려면 *args를 받는 함수를 확장할 때 키워드 전용(keyword-only)인수를 사용해야 한다.

핵심정리

  • def문에서 *args를 사용하면 함수에서 가변 개수의 위치 인수를 받을 수 있다.
    • 연산자를 쓰면 시퀀스에 들어있는 아이템을 함수의 위치 인수로 사용할 수 있다.
  • 제너레이터와 * 연산자를 함께 사용하면 프로그램이 메모리 부족으로 망가질 수도 있다.
  • *args를 받는 함수에 새 위치 파라미터를 추가하면 정말 찾기 어려운 버그가 생길 수도 있다.

Comment  Read more

이펙티브 파이썬 - 17. 인수를 순회할 때는 방어적으로 하자

|

# 기본적인 정규화 함수
def normalize(numbers):
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * float(value) / total
        result.append(percent)
    return result

visits = [15, 34, 53, 25, 17, 194]
percentages = normalize(visits)
print(percentages)
[4.437869822485207, 10.059171597633137, 15.680473372781066, 7.396449704142012, 5.029585798816568, 57.396449704142015]
# visitor 데이터를 읽는 제너레이터 함수 정의
# 제너레이터로 만들어야지 visitor가 엄청엄청 커도 계산이 가능 (betterway 16 참고)
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 예외를 일으킨 이터레이터나 제너레이터를 순회하면 어떤 결과도 얻을 수 없다.
it2 = read_visits('./my_numbers.txt')
print(list(it2))
print(list(it2))
[3546, 246, 25436, 7356, 35432, 25, 2566, 24632, 264, 25235, 662, 2, 262, 5234, 5263, 236, 26456, 25, 23523, 52, 45, 62, 52, 72, 23, 52, 52, 53, 734, 645, 6345, 6346, 63, 742, 6234, 64, 724, 6245]
[]
  • 이미 소진한 이터레이터를 순회해도 오류가 뜨지 않음
  • for, list 등과 같은 파이썬 표준 라이브러리의 많은 함수는 정상적인 동작과정에서 StopIteration 예외가 일어 날 수 있다고 가정
  • 이런 함수는 결과가 없는 이터레이터와 결과가 있었지만 이미 소진한 이터레이터의 차이를 알려주지 않는다.
# 위 문제를 해결하기 위해 입력 이터레이터를 명시적으로 소진 후 전체 콘텐츠의 복사본을 리스트에 저장
# 그렇게하면 리스트 버전의 데이터를 필요한 만큼 순회 할 수 있다.
# 1번 cell과 같은 함수지만 입력 이터레이터를 방어적으로 복사하는 함수임.
def normalize_copy(numbers):
    numbers = list(numbers)
    total = float(sum(numbers))
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result
it3 = read_visits('./my_numbers.txt')
percentages = normalize_copy(it3)
print(percentages) # 제대로 동작 함
[1.6492562998241909, 0.11441541166293033, 11.830367524627219, 3.421299870701283, 16.47954010585751, 0.011627582486070156, 1.1934550663702408, 11.456424471875204, 0.12278727105290085, 11.736881761439216, 0.30789838423113775, 0.0009302065988856125, 0.12185706445401524, 2.434350669283648, 2.4478386649674895, 0.10976437866850228, 12.304772890058882, 0.011627582486070156, 10.940624912793131, 0.024185371571025925, 0.02092964847492628, 0.02883640456545399, 0.024185371571025925, 0.03348743755988205, 0.010697375887184543, 0.024185371571025925, 0.024185371571025925, 0.02465047487046873, 0.3413858217910198, 0.29999162814061003, 2.951080434964606, 2.9515455382640483, 0.029301507864896794, 0.3451066481865622, 2.899453968726454, 0.0297666111643396, 0.33673478879659174, 2.904570105020325]
# 위 방법의 문제는 입력받은 이터레이터 콘텐츠의 복사본이 클 수도 있다는 점
# 저렇게 할거면 그냥 read_visits를 리스트로 받아오는게....
# 만약 my_numbers.txt가 엄청 크면 OOM 뜰 수 있음 (list의 단점)
# 이 문제를 피하려면, 호출 될 때마다 새 이터레이터를 반환하는 함수를 받게 만드는 것
def normalize_func(get_iter):
    total = sum(get_iter()) # 새 이터레이터, total을 계산하는데 필요한 sum 함수를 위한
    result = []
    for value in get_iter(): # 새 이터레이터, 다시 처음부터 percent 계산을 위해 value에 대한
        percent = 100 * value / total
        result.append(percent)
    return result
# normalize_func을 사용하려면 제너레이터를 호출해서 매번 새 이터레이터를 생성하는 람다 표현식을 넘겨주면 됨
path = "./my_numbers.txt"
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
[1.6492562998241909, 0.11441541166293033, 11.830367524627219, 3.421299870701283, 16.47954010585751, 0.011627582486070156, 1.1934550663702408, 11.456424471875204, 0.12278727105290085, 11.736881761439216, 0.30789838423113775, 0.0009302065988856125, 0.12185706445401524, 2.434350669283648, 2.4478386649674895, 0.10976437866850228, 12.304772890058882, 0.011627582486070156, 10.940624912793131, 0.024185371571025925, 0.02092964847492628, 0.02883640456545399, 0.024185371571025925, 0.03348743755988205, 0.010697375887184543, 0.024185371571025925, 0.024185371571025925, 0.02465047487046873, 0.3413858217910198, 0.29999162814061003, 2.951080434964606, 2.9515455382640483, 0.029301507864896794, 0.3451066481865622, 2.899453968726454, 0.0297666111643396, 0.33673478879659174, 2.904570105020325]
  • 잘 동작하긴 함….
  • 근데 님들… 람다함수 잘(자주) 쓰나여…?
  • 이렇게 람다 함수를 넘겨주는 방법은 세련되지 못함
  • 같은 결과를 얻는 더 좋은 방법은 이터레이터 프로토콜(iterator protocol)을 구현한 새 컨테이너 클래스를 제공하는 것

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

  • 파이썬은 for x in foo 같은 문장을 만나면 실제로는 iter(foo)를 호출한다.
  • 내장함수 iter는 특별한 메서드인 foo.__iter__를 호출
  • __iter__메서드(__next__라는 특별한 메서드를 구현하는)는 이터레이터 객체를 반환해야 함
  • for 루프는 이터레이터를 모두 소진할 때까지 (StopIteration 예외가 발생할 때까지) 이터레이터 객체에 내장함수 next를 계속 호출
# 복잡해 보이지만 사실 클래스의 __iter__ 메서드를 제너레이터를 구현하면 됨
# 여행자 데이터를 담은 파일을 읽는 이터러블(iterable:순회가능) 컨테이너 클래스.
class ReadVisits(object):
    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)
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
[1.6492562998241909, 0.11441541166293033, 11.830367524627219, 3.421299870701283, 16.47954010585751, 0.011627582486070156, 1.1934550663702408, 11.456424471875204, 0.12278727105290085, 11.736881761439216, 0.30789838423113775, 0.0009302065988856125, 0.12185706445401524, 2.434350669283648, 2.4478386649674895, 0.10976437866850228, 12.304772890058882, 0.011627582486070156, 10.940624912793131, 0.024185371571025925, 0.02092964847492628, 0.02883640456545399, 0.024185371571025925, 0.03348743755988205, 0.010697375887184543, 0.024185371571025925, 0.024185371571025925, 0.02465047487046873, 0.3413858217910198, 0.29999162814061003, 2.951080434964606, 2.9515455382640483, 0.029301507864896794, 0.3451066481865622, 2.899453968726454, 0.0297666111643396, 0.33673478879659174, 2.904570105020325]
  • 위 코드가 동작하는 이유는, normalizesum 메서드가 새 이터레이터 객체를 할당하려고 ReadVisits.__iter__를 호출하기 때문
  • 숫자를 정규화하는 for 루프도 두번째 이터레이터 객체를 할당할 때 __iter__를 호출한다.
  • 위 두 이터레이터는 독립적으로 동작하므로 각각의 순회 과정에서 모든 입력 데이터 값을 얻을 수 있다.
  • 이 방법의 유일한 단점은 입력 데이터를 여러 번 읽는다는 점이다.

  • 이제 ReadVisits와 같은 컨테이너가 어떻게 동작하는지 알았으니, 파라미터가 단순한 이터레이터가 아님을 보장하는 함수를 작성해야 함
    • 프로토콜에 따르면 내장함수 iter에 이터레이터를 넘기면 이터레이터 자체가 반환
    • 반면에 iter에 컨테이너 타입을 넘기면 매번 새 이터레이터 객체가 반환 됨
    • 따라서 이 동작으로 입력값을 테스트해서 이터레이터면 TypeError를 일으켜 거부하게 만들면 된다.
def normalize_defensive(numbers):
    if iter(numbers) is iter(numbers): # 이터레이터 -- 거부!
        raise TypeError('Must supply a container')
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result
  • normalize_defensivenormalize_copy처럼 입력 이터레이터 전체를 복사하고 싶지 않지만, 입력 데이터를 여러번 순회해야 할 때 사용하면 좋다.
    • 이 함수는 listReadVisits를 입력으로 받으면 입력이 컨테이너이므로 기대한 대로 동작
    • 이터레이터 프로토콜을 따르는 어떤 컨테이너 타입에 대해서도 제대로 동작
visits = [15, 35, 80]
normalize_defensive(visits) # 오류 없음
visits = ReadVisits(path)
normalize_defensive(visits) # 오류 없음
[1.6492562998241909,
 0.11441541166293033,
 11.830367524627219,
 3.421299870701283,
 16.47954010585751,
 0.011627582486070156,
 1.1934550663702408,
 11.456424471875204,
 0.12278727105290085,
 11.736881761439216,
 0.30789838423113775,
 0.0009302065988856125,
 0.12185706445401524,
 2.434350669283648,
 2.4478386649674895,
 0.10976437866850228,
 12.304772890058882,
 0.011627582486070156,
 10.940624912793131,
 0.024185371571025925,
 0.02092964847492628,
 0.02883640456545399,
 0.024185371571025925,
 0.03348743755988205,
 0.010697375887184543,
 0.024185371571025925,
 0.024185371571025925,
 0.02465047487046873,
 0.3413858217910198,
 0.29999162814061003,
 2.951080434964606,
 2.9515455382640483,
 0.029301507864896794,
 0.3451066481865622,
 2.899453968726454,
 0.0297666111643396,
 0.33673478879659174,
 2.904570105020325]
# 함수는 입력이 이터러블이어도 컨테이너가 아니면 예외를 일으킨다
it = iter(visits)
normalize_defensive(it)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-15-d893faf1a58b> in <module>()
      1 # 함수는 입력이 이터러블이어도 컨테이너가 아니면 예외를 일으킨다
      2 it = iter(visits)
----> 3 normalize_defensive(it)


<ipython-input-13-462b037875ce> in normalize_defensive(numbers)
      1 def normalize_defensive(numbers):
      2     if iter(numbers) is iter(numbers): # 이터레이터 -- 거부!
----> 3         raise TypeError('Must supply a container')
      4     total = sum(numbers)
      5     result = []


TypeError: Must supply a container

핵심 정리

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

Comment  Read more