defaultdict : 조건 없는 딕셔너리의 진화

파이썬에는 수많은 외부 라이브러리가 존재하지만, 표준 라이브러리만 제대로 이해해도 실무에서 충분히 강력한 코드를 작성할 수 있습니다. 이번 글에서는 그중에서도 collections.defaultdict를 깊이 있게 살펴보겠습니다.

이 글을 통해 단순한 개념 소개를 넘어서, 실제로 언제, 왜, 어떻게 defaultdict를 써야 하는지를 명확히 이해하게 될 것입니다.

collections시리즈인 첫번째, Counter클래스에 대해 궁금하신 분은 이전 글을 읽어보시길 추천드립니다. Python표준 라이브러리 ① - collections.Counter


1. 기본 개념: defaultdict란?

Tux with defaultdicts

defaultdict는 파이썬 표준 라이브러리 collections 모듈에 포함된 특수한 딕셔너리(dict)의 서브클래스입니다. 일반적인 딕셔너리에서는 존재하지 않는 키에 접근할 경우 KeyError가 발생하지만, defaultdict자동으로 기본값을 생성해주는 함수(factory function)를 지정할 수 있어 코드가 훨씬 깔끔해지고 오류를 방지할 수 있습니다.


2. 기본 사용법

from collections import defaultdict

d = defaultdict(int)
d['apple'] += 1
print(d)  # defaultdict(<class 'int'>, {'apple': 1})

여기서 int()는 기본값으로 0을 리턴합니다. 존재하지 않는 키 'apple'에 접근할 때 KeyError 없이 0을 자동으로 생성한 뒤 +1이 수행된 것입니다.


3. 다양한 기본값 예시

from collections import defaultdict

# 기본값: 0 (int)
counter = defaultdict(int)
counter['a'] += 1
print(counter)  # defaultdict(<class 'int'>, {'a': 1})

# 기본값: 빈 리스트
group = defaultdict(list)
group['fruit'].append('apple')
group['fruit'].append('banana')
print(group)  # defaultdict(<class 'list'>, {'fruit': ['apple', 'banana']})

# 기본값: 빈 집합
unique_tags = defaultdict(set)
unique_tags['tags'].add('python')
unique_tags['tags'].add('coding')
print(unique_tags)  # defaultdict(<class 'set'>, {'tags': {'python', 'coding'}})

# 기본값: 커스텀 초기값
fixed = defaultdict(lambda: 100)
print(fixed['unknown'])  # 100

4. 실전 예시

1. 단어 빈도수 세기

words = ['apple', 'banana', 'apple', 'orange', 'banana']
counter = defaultdict(int)

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

print(counter)
# defaultdict(<class 'int'>, {'apple': 2, 'banana': 2, 'orange': 1})

👉 Counter vs defaultdict
단어의 빈도수 세기는 collections.Counter()가 더욱 특화되어있기에 통계나 순위 분석이 필요한 경우에는 Counter를 사용하는 것이 바람직합니다. 하지만 누적 카운팅처럼 단순한 합산에는 defaultdict(int)도 충분히 간결하게 사용 가능합니다.

2. 로그를 그룹별로 정리하기

logs = [
    ('2024-01-01', 'INFO'),
    ('2024-01-01', 'ERROR'),
    ('2024-01-02', 'DEBUG'),
]

grouped = defaultdict(list)
for date, level in logs:
    grouped[date].append(level)

print(grouped)
# defaultdict(<class 'list'>, {'2024-01-01': ['INFO', 'ERROR'], '2024-01-02': ['DEBUG']})

3. 중복 제거된 태그 정리

entries = [
    ('post1', 'python'),
    ('post1', 'coding'),
    ('post1', 'python'),
]

tags = defaultdict(set)
for post, tag in entries:
    tags[post].add(tag)

print(tags)
# defaultdict(<class 'set'>, {'post1': {'python', 'coding'}})

5. 주의 사항

  • defaultdict는 내부적으로 기본값 생성기를 저장하므로, repr() 시 일반 dict와 다른 모양이 나올 수 있습니다.
  • JSON 직렬화 시 문제가 될 수 있습니다. dict(d)로 변환 후 처리하는 것이 안전합니다.
  • [] 접근 시에만 기본값이 생성됩니다. get()으로 접근하면 생성되지 않습니다.
from collections import defaultdict

d = defaultdict(list)
print(d.get('missing'))  # None
print(d['missing'])      # []

6. 언제 쓰면 좋은가? – defaultdict만의 결정적 장점 3가지

defaultdictdict + 조건문 패턴을 많이 사용하는 상황에서 가독성, 유지보수성, 안전성을 동시에 높여줍니다. 특히 다음 3가지 상황에서 '아, 이건 무조건 defaultdict 쓰는 게 낫다!'고 느끼게 됩니다.

6-1. 조건 없이 카운트/누적하는 집계 코드

from collections import defaultdict

# 일반 dict
counts = {}
for item in items:
    if item not in counts:
        counts[item] = 0
    counts[item] += 1

# defaultdict
counts = defaultdict(int)
for item in items:
    counts[item] += 1

✔ 조건문이 사라지면서 코드가 간결해지고 실수 여지도 줄어듭니다.
✔ 특히 로그 분석, 단어 카운팅 등 대량 데이터 처리에 매우 적합합니다.

6-2. 리스트/집합을 누적할 때 setdefault를 대체

from collections import defaultdict

# 일반 dict
posts = {}
for tag, post in data:
    if tag not in posts:
        posts[tag] = []
    posts[tag].append(post)

# defaultdict
posts = defaultdict(list)
for tag, post in data:
    posts[tag].append(post)

setdefault()보다 훨씬 직관적이고 반복문 안에서도 깔끔합니다.
데이터 그룹핑에 최적화된 구조입니다.

6-3. 중첩 딕셔너리 구성시 초기화를 자동화

# 일반 딕셔너리
matrix = {}
if 'x' not in matrix:
    matrix['x'] = {}
matrix['x']['y'] = 10

# defaultdict 중첩
matrix = defaultdict(lambda: defaultdict(int))  # 키가 없을 때마다 내부에 defaultdict(int)가 자동 생성됨
matrix['x']['y'] += 10

✔ 중첩된 자료구조를 손쉽게 만들 수 있어 다차원 딕셔너리 작업에 매우 유리합니다.
✔ 데이터 마이닝, 파싱, 트리 구조 저장 등에 강력한 장점을 발휘합니다.

lambda: defaultdict(int)는 내부적으로 키가 없을 때마다 defaultdict(int)를 반환하는 구조로, 자동으로 딕셔너리가 중첩 생성됩니다.


7. 정리

collections.defaultdict는 초보자에게는 단순한 dict 확장판처럼 보일 수 있지만, 쓰면 쓸수록 코드를 더 명확하고 안전하게 만드는 구조적 도구라는 사실을 체감하게 됩니다.

  • KeyError를 걱정하지 않고 딕셔너리를 쓸 수 있다.
  • 조건문 없이 데이터를 그룹화하고 누적할 수 있다.
  • 중첩된 딕셔너리를 직관적으로 구성할 수 있다.
# defaultdict로 안정적이고 간결하게 처리하는 예시
salaries = defaultdict(int)
for dept, amount in records:
    salaries[dept] += amount

한 줄의 코드로 오류 방지 + 가독성 향상 + 유지보수성까지 잡을 수 있다면,
defaultdict는 단순한 편의 기능이 아니라 파이썬다운 사고 방식의 핵심 도구라고 할 수 있습니다.

다음에 다룰 주제는 pathlib입니다.
파일/디렉토리 다루는 코드를 객체지향적으로 바꿔주는 모던한 방식으로, 초보부터 중급까지 모두 유용하게 활용가능합니다. os.path에 익숙한 개발자분들은 "와우! os.path보다 훨씬 간단하네!"라는 기분을 느끼실 것입니다. 다음 편도 기대해주세요.