|

파이썬 defaultdict 쉽게 이해하기

파이썬 defaultdict 대표 이미지
counting과 grouping 코드를 더 읽기 쉽게 만드는 기본기

파이썬 defaultdict는 사소한 편의 문법처럼 보이지만, 실제로는 없는 키 초기화 패턴을 한 곳에 모아주는 dict subclass입니다. 그래서 카운팅, 그룹핑, 버킷 만들기 코드가 꽤 많이 단순해집니다.

먼저 핵심부터 말하면 defaultdict는 없는 키를 만났을 때 어떤 기본값을 만들지 default_factory로 미리 선언해두는 구조이며, d[key] 접근에서만 그 자동 생성이 동작합니다.

defaultdict 요약 카드
핵심 카드: factory, counting, grouping, setdefault 비교, 주의점

파이썬 defaultdict를 한 줄로 이해하기

Python 공식 문서는 defaultdict를 missing key에 대해 factory function을 호출해 기본값을 공급하는 dict subclass로 설명합니다. 즉, 일반 dict와 거의 같지만 없는 키를 만났을 때 기본값 생성 규칙이 추가된 구조입니다.

from collections import defaultdict

counts = defaultdict(int)
counts["apple"] += 1
counts["apple"] += 1

print(counts["apple"])  # 2

여기서 int는 0을 만들어주는 factory로 쓰입니다. 일반 dict라면 먼저 0으로 초기화하는 분기문이 필요하지만, defaultdict는 그 규칙을 선언 시점에 한 번 적어두면 됩니다.


일반 dict에서는 왜 코드가 길어질까

words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = {}

for word in words:
    if word not in counts:
        counts[word] = 0
    counts[word] += 1

print(counts)

이 코드는 맞습니다. 하지만 핵심 로직은 개수를 늘리는 것인데, 그 전에 없는 키를 초기화하는 코드가 반복됩니다.

from collections import defaultdict

words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = defaultdict(int)

for word in words:
    counts[word] += 1

print(counts)

defaultdict 버전은 코드의 중심이 더 분명합니다. 단어를 세어라라는 의도가 바로 보입니다.


default_factory와 __missing__은 실제로 어떻게 동작할까

공식 문서 기준으로 default_factory가 None이면 없는 키 접근 시 KeyError가 납니다. None이 아니면 인자 없이 호출해 기본값을 만들고, 그 값을 딕셔너리에 넣은 뒤 반환합니다.

from collections import defaultdict

groups = defaultdict(list)
groups["python"].append("dict")
groups["python"].append("set")

a = groups["java"]
print(a)
print(groups)

없는 키를 읽는 순간 기본값이 실제 딕셔너리에 들어간다는 점을 기억하면 동작을 훨씬 덜 헷갈립니다.

defaultdict 동작 흐름 다이어그램
d[key] 접근 시 기본값 생성과 저장 흐름

defaultdict(int)는 카운팅에서 특히 편하다

from collections import defaultdict

scores = ["A", "B", "A", "C", "B", "A"]
count_by_grade = defaultdict(int)

for grade in scores:
    count_by_grade[grade] += 1

print(count_by_grade)

여기서 int는 0을 만드는 factory입니다. 처음 보는 키면 0으로 시작하고, 이후에는 그냥 1을 더합니다. 카운팅 코드에서 if문을 없애는 가장 전형적인 패턴입니다.


defaultdict(list)는 그룹핑에서 빛난다

from collections import defaultdict

fruits = ["apple", "apricot", "banana", "blueberry", "avocado"]
groups = defaultdict(list)

for fruit in fruits:
    groups[fruit[0]].append(fruit)

print(groups)

list factory는 같은 키 아래에 여러 값을 모을 때 자연스럽습니다. append만 생각하면 되기 때문에 그룹핑 의도가 아주 또렷하게 드러납니다.


defaultdict(set)은 중복 없는 버킷을 만들 때 좋다

from collections import defaultdict

pairs = [
    ("python", "dict"),
    ("python", "set"),
    ("python", "dict"),
    ("java", "map"),
]

tech_map = defaultdict(set)

for language, topic in pairs:
    tech_map[language].add(topic)

print(tech_map)

set factory는 그룹핑은 하되 중복은 제거하고 싶을 때 잘 맞습니다. list와 set 중 무엇을 고를지는 보존하고 싶은 정보가 순서인지, 중복 없는 membership인지에 따라 달라집니다.


setdefault와는 무엇이 다를까

공식 문서에 따르면 dict.setdefault(key, default)는 키가 있으면 기존 값을 반환하고, 없으면 default를 넣고 그 값을 반환합니다. 그래서 그룹핑 코드를 아래처럼도 쓸 수 있습니다.

fruits = ["apple", "apricot", "banana", "blueberry", "avocado"]
groups = {}

for fruit in fruits:
    groups.setdefault(fruit[0], []).append(fruit)

print(groups)

setdefault도 충분히 쓸 만합니다. 다만 반복문 안에서 같은 패턴을 계속 적게 되므로, 반복이 길어질수록 defaultdict가 더 읽기 편한 경우가 많습니다.

defaultdict와 setdefault 비교 카드
짧은 비교: 선언 위치, 반복 코드, 읽기 쉬움, 잘 맞는 장면

꼭 알아야 하는 주의점: get()은 default_factory를 쓰지 않는다

Python 공식 문서는 __missing__이 __getitem__ 경로에서만 호출된다고 설명합니다. 그래서 d[key] 접근은 기본값 자동 생성이 가능하지만, d.get(key)는 일반 dict처럼 동작합니다.

from collections import defaultdict

d = defaultdict(list)

print(d.get("python"))   # None
print(d)                  # 아직 비어 있음

print(d["python"])       # []
print(d)                  # 이제 키가 생김

이 차이를 모르면 get도 자동 생성해줄 거라고 기대했다가 헷갈리기 쉽습니다. 조회만 할지, 없으면 실제로 버킷을 만들지 의도를 분명히 나누는 편이 좋습니다.


defaultdict가 항상 정답은 아니다

  • 없는 키를 만나는 것 자체가 버그인 경우에는 일반 dict가 더 자연스럽습니다.
  • 카운팅만 하고 싶다면 Counter가 더 직접적일 수 있습니다.
  • 기본값 규칙이 문맥마다 자주 달라진다면 자동 생성보다 명시적 분기가 더 읽기 쉬울 수 있습니다.
from collections import Counter

words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = Counter(words)
print(counts)

즉 단순 빈도수 집계는 Counter, 그룹핑/버킷/커스텀 누적은 defaultdict라고 생각하면 꽤 편합니다.


한 번에 기억하는 선택 기준

  1. 없는 키를 만났을 때 같은 기본값 규칙이 계속 반복되는가?
  2. 카운팅, 그룹핑, 버킷 생성이 핵심인가?
  3. if key not in d 또는 setdefault가 반복되어 핵심 로직이 흐려지는가?

이 질문에 예가 많다면 defaultdict가 잘 맞을 가능성이 큽니다. 반대로 없는 키를 만나는 순간 바로 오류를 보고 싶은 경우에는 일반 dict가 더 자연스러울 수 있습니다.


함께 보면 좋은 글과 공식 문서

기본 문법 흐름을 더 정리하고 싶다면 파이썬 pass, continue, break 차이, 파이썬 is와 == 차이도 같이 읽어보면 좋습니다.

공식 기준은 collections.defaultdict 문서dict.setdefault 문서를 참고하면 됩니다.


결론: defaultdict는 기본값 규칙을 선언해두는 dict다

파이썬 defaultdict는 없는 키를 만났을 때 어떤 기본값을 만들지 미리 선언해두는 dict subclass입니다. 그래서 카운팅, 그룹핑, 중복 없는 버킷처럼 누락 키 초기화가 반복되는 장면에서 특히 편합니다. 다만 get은 자동 생성하지 않는다는 규칙까지 같이 기억하면 훨씬 안전하게 쓸 수 있습니다.

함께보면 좋은 글