ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 동적 기본 인수를 지정하려면 None과 docstring을 사용
    언어/파이썬 & 장고 2016. 10. 21. 20:54

    키워드 인수의 기본값으로 비정적(non-static)타입을 사용해야 할 때도 있습니다.

    예를 들어 이벤트 발생 시각까지 포함해 로깅 메시지를 출력한다고 가정합니다. 보통 함수를 호출한 시각을 메시지에 포함하여 다음과 같이 처리합니다.

    import datetime
    import time
    
    def log(message, when=datetime.datetime.now()):
        print('%s: %s' % (when, message))
    
    log('hi there')
    time.sleep(0.1)
    log('hi again')
    
    # 결과
    # 2016-10-21 20:40:07.689163: hi there
    # 2016-10-21 20:40:07.689163: hi again

    datetime.datetime.now()는 함수를 정의할 때 딱 한번만 실행되므로 타임스탬프가 변하지 않습니다. 기본 인수의 값은 모듈이 로드될 때 한 번만 평가되며 보통 프로그램이 시작할 때 일어납니다. 이 코드를 담고있는 모듈이 로드된 후에는 datetime.datetime.now()를 다시 평가하지 않습니다.


    파이썬에서 결과가 기대한 대로 나오게 하려면 기본값을 None으로 설정하고 docstring으로 실제 동작을 문서화하는 게 관례입니다.

    import datetime
    import time
    
    
    def log(message, when=None):
        """
        log a message with a timestamp
        :param message: 메세지 출력
        :param when: 메세지 출력할 때 시간. 기본으로 현재시간
        """
        when = datetime.datetime.now() if when is None else when
        print('%s: %s' % (when, message))
    
    
    log('hi there')
    time.sleep(0.1)
    log('hi again')
    
    # 결과
    # 2016-10-21 20:44:03.966362: hi there
    # 2016-10-21 20:44:04.068298: hi again


    기본 인수값으로 None을 사용하는 방법은 인수가 수정 가능할 때 특히 중요합니다. 예를 들어 JSON 데이터로 인코드된 값을 로드한다고 할 때, 데이터 디코딩이 실패하면 기본값으로 빈 딕셔너리를 반환하려고하면 다음과 같이 할 수 있습니다.

    import json
    
    def decode(data, default={}):
        try:
            return json.loads(data)
        except ValueError:
            return default

    위 코드는 datetime.datetime.now()와 같은 문제가 있습니다. 기본 인수 값을 딱 한번만 평가되므로 기본값으로 설정한 딕셔너리를 모든 decode 호출에서 공유합니다. 이 문제는 예상치 못한 동작을 유발합니다.


    import json
    
    def decode(data, 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, 'meep': 1})
    # ('bar: ', {'stuff': 5, 'meep': 1})


    각각 단일 키와 값을 담은 서로 다른 딕셔너리 두 개를 예상했지만 하나를 수정하면 다른 하나도 수정된 것처럼 보입니다. 이런 문제의 원인은 foo와 bar 둘 다 기본 파라미터와 같다는 점입니다. 이 둘은 같은 딕셔너리 객체입니다.

    assert foo is bar


    키워드 인수의 기본값을 None으로 설정하고 함수의 docstring에 동작을 문서화해서 이 문제를 고쳐야 합니다.

    import json
    
    
    def decode(data, default=None):
        """
        스트링을 json 데이터로 load
        :param data: decode할 json 데이터
        :param default: json데이터
        :return:
        """
        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으로 실제 동작을 문서화해야 이해가 쉬움


    댓글