-
[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으로 실제 동작을 문서화해야 이해가 쉬움