기본 인자에서 먼저 볼 것은 "기본값이 있느냐"가 아니라 그 값이 함수 정의 시점에 고정되어도 괜찮으냐입니다.
숫자, 문자열, 불리언, None처럼 안전한 기본값과 리스트, dict, set처럼 공유 버그를 만들 수 있는 기본값을 먼저 나누면 실수가 크게 줄어듭니다.
숏컷 코드
def add_tag(name: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(name)
return tags문법
기본형은 기본값 없음, 직접 기본값, None 패턴 세 가지를 함께 보면 됩니다.
def require_name(name):
return name.strip()
def bump(value, step=1):
return value + step
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket기본 인수 표현식은 호출할 때마다 다시 계산되지 않고, 함수가 정의될 때 한 번 평가됩니다.
def bump(value, step=1):
return value + step숫자나 문자열 같은 불변 값은 괜찮지만, 리스트나 dict처럼 상태가 바뀌는 객체는 같은 인스턴스를 여러 호출이 공유하게 될 수 있습니다.
기본값 안전성
어떤 기본값이 안전하고 어떤 기본값이 위험한가
숫자, 문자열, 불리언, None처럼 불변으로 다루는 값은 기본값으로 직접 두기 비교적 안전합니다.
def bump(value, step=1):
return value + step
def greet(name, prefix="Hello"):
return f"{prefix}, {name}"핵심은 "이 값이 호출 사이에 공유되어도 문제없는가"입니다.
리스트, dict, set 같은 가변 기본값은 공유 버그를 만들 수 있습니다.
def add_item(item, bucket=[]):
bucket.append(item)
return bucket이 코드는 호출마다 새 리스트가 생길 것처럼 보이지만, 실제로는 같은 리스트가 계속 재사용됩니다.
호출마다 새 컨테이너가 필요하면 None 패턴이 기본입니다.
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket이렇게 하면 각 호출이 독립적인 기본 컨테이너를 갖게 됩니다.
sentinel
None 대신 언제 sentinel이 필요한가
None 자체도 정상 입력이라면 None을 "전달 안 됨" 신호로 쓰면 안 됩니다.
이때는 별도 sentinel 객체를 두는 편이 더 정확합니다.
_MISSING = object()
def load_config(path=_MISSING):
if path is _MISSING:
path = default_config_path()
return path설정 경로처럼 "None도 의미 있는 값"인 코드에서는 이 차이가 중요합니다.
주의할 점
가변 객체를 기본 인수로 직접 두면 호출 사이에 상태가 공유됩니다.
# ❌ 여러 호출이 같은 리스트를 공유
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b']
# ✅ None 패턴으로 매 호출 독립 보장
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket"왜 상태가 누적되지?"가 보이면 가변 기본값부터 먼저 의심하는 편이 좋습니다.
호출마다 새 리스트나 새 dict가 생길 것처럼 보이는 함수에서 가장 자주 나오는 버그입니다.
참고 링크
2 sources