키워드 인수의 기본값으로 비정적타입을 사용해야 할 때도 있다.
예. 이벤트 발생 시각까지 포함해 로깅 메시지를 출력한다고 하자. 기본적인 경우에는 함수를 호출한 시각을 메시지에 포함하려고 한다. 함수가 호출될 때 마다 기본 인수를 평가한다고 가정하고 다음과 같이 처리하려 할 것이다.
from time import sleep
from datetime import datetime
def log(message, when=datetime.now()):
print('%s: %s' % (when, message))
log('Hi there!')
sleep(0.1)
log('Hi again!')
2020-12-10 09:27:33.260913: Hi there!
2020-12-10 09:27:33.260913: 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))
2020-12-10 09:39:25.328102: Hi there!
2020-12-10 09:39:25.428109: Hi again!
이제는 타임스탬프가 다르게 나오는 것이 확인된다.
기본 인수 값으로 None을 사용하는 방법은 인수가 수정 가능(mutable)할 때 특히 중요함. 예. JSON 데이터로 인코드된 값을 로드한다고 하자.
데이터 디코딩이 실패하면 기본값으로 빈 딕셔너리를 반환하려고 한다.
다음과 같은 방법을 써볼 수 있다.
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}
위의 코드에는 datetime.now 예제와 같은 문제가 있다. 기본 인수 값은(모듈이 로드될 때) 딱 한 번만 평가되므로, 기본값으로 설정한 딕셔너리를 모든 decode 호출에서 공유한다. 보는 바와 같이 이 문제는 예상치 못한 동작을 야기했어요.
아마 각각 단일 키와 값을 담은 서로 다른 딕셔너리 두 개를 예상했을 것이다. 하지만 하나를 수정하면 다른 하나도 수정되는 것처럼 보인다. 이런 문제의 원인은 foo와 bar 둘 다 기본 파라미터와 같다는 점이다. 즉, 이 둘은 같은 딕셔너리 객체다.
assert foo is bar
키워드 인수의 기본값을 None으로 설정하고 함수의 docstring에 동작을 문서화해서 이 문제를 고친다.
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}