setdefault는 못생긴 dict 메서드
Source: Dev.to
Introduction
가끔씩 파이썬 퀴즈를 풀면서 실력을 유지하고 새로운 언어 기능을 발견합니다. 어느 퀴즈에서는 다음 코드의 반환값을 물었습니다.
{i: i**2 for i in range(3)}.setdefault(2, 10)
setdefault()를 한 번도 사용해 본 적이 없어서 선택지 2, 10, 4, 1 중에서 추측했죠. 정답은 4였지만, 왜 그런지 확실히 알지 못했습니다.
How setdefault() Works
파이썬 문서에는 다음과 같이 적혀 있습니다:
If key is in the dictionary, return its value. If not, insert key with a value of default and return default. default defaults to
None.
위 표현을 적용하면:
>>> d = {i: i**2 for i in range(3)} # {0: 0, 1: 1, 2: 4}
>>> d.setdefault(2, 10)
4
키 2가 이미 존재하므로 setdefault는 기존 값(4)을 반환하고 사전을 변경하지 않습니다.
Why the Method Is Named setdefault
메서드 이름은 수행되는 두 가지 동작을 반영합니다:
- Set the key to a default value if the key is missing.
- Return the value associated with the key (whether it was already present or just set).
.get(key, default)는 값을 단순히 조회할 뿐이지만, setdefault는 호출 후 키가 반드시 존재하도록 보장하므로, 값을 조회하고 필요 시 초기화해야 하는 코드를 간결하게 만들 수 있습니다.
Typical Use Case
리스트를 값으로 갖는 사전을 만들 때 흔히 쓰이는 패턴입니다:
items_by_color = {}
items = [('duck', 'purple'), ('water bottle', 'purple'), ('uni-duck', 'pink')]
for item_name, item_color in items:
# Get the list for the colour, or set it to a new empty list and get that
items_list = items_by_color.setdefault(item_color, [])
items_list.append(item_name)
# Result: {'purple': ['duck', 'water bottle'], 'pink': ['uni-duck']}
여기서 setdefault는 리스트에 항목을 추가하기 전에 명시적인 if … else 검사를 할 필요를 없애 줍니다.
Performance Considerations
setdefault를 사용한 경우와 명시적인 if … else 블록을 사용한 경우의 성능을 비교해 보았습니다.
def test_no_set_default(sequence: dict) -> dict:
"""Re‑order incoming dict by value using if..else."""
reversed = {}
for k, v in sequence.items():
if v not in reversed:
reversed[v] = [k]
else:
reversed[v].append(k)
return reversed
def test_set_default(sequence: dict) -> dict:
"""Re‑order incoming dict by value using setdefault."""
reversed = {}
for k, v in sequence.items():
reversed.setdefault(v, [])
reversed[v].append(k)
return reversed
무작위 데이터를 생성하는 도우미 함수:
from faker import Faker
def make_dict(items: int = 1000) -> dict:
"""Create a dictionary of random values."""
fake = Faker()
return {fake.name(): fake.color_name() for _ in range(items)}
두 함수를 타이밍해 보기:
from timeit import timeit
def test_calls(times: int = 1_000_000):
setup_dict = make_dict()
t1 = timeit(lambda: test_no_set_default(setup_dict), number=times)
t2 = timeit(lambda: test_set_default(setup_dict), number=times)
print(f"no setdefault: {t1}")
print(f"with setdefault: {t2}")
샘플 출력:
$ uv run python3 set_default_comparison.py --times 1000000
no setdefault: 100.65228875500907
with setdefault: 136.7275900120003
이 마이크로‑벤치마크에서는 setdefault 버전이 약 30 % 정도의 오버헤드를 보였습니다. 많은 스크립트에서는 차이가 미미할 수 있지만, 성능이 중요한 코드에서는 영향을 줄 수 있습니다.
Conclusion
setdefault는 누락된 키를 초기화하고 그 값을 반환하는 동작을 하나로 결합해 코드를 더 간결하고 표현력 있게 만들 수 있습니다. 하지만 명시적인 if … else 검사에 비해 약간의 실행 시간 비용이 발생합니다. 가독성과 간결성이 약간의 성능 저하보다 중요할 때 사용하고, 마이크로초 단위까지 최적화가 필요한 핫 경로에서는 명시적인 패턴을 선호하세요.