Skip to content

Python defaultdict: 기본값으로 딕셔너리 연산 간소화하기

Updated on

모든 Python 개발자가 이 벽에 부딪힌 적이 있습니다: 딕셔너리를 사용하여 항목을 그룹화하거나 카운트하는 깔끔한 루프를 작성하고 코드를 실행하면, 키가 아직 존재하지 않아서 KeyError가 전체 스크립트를 중단시킵니다. 일반적인 해결 방법은 곳곳에 if key in dict 검사나 try/except KeyError 블록을 뿌리는 것입니다. 10줄의 데이터를 그룹화하는 로직이 갑자기 20줄의 방어적 보일러플레이트 코드로 부풀어 오릅니다.

이 문제는 규모가 커질수록 악화됩니다. 그래프의 인접 리스트를 구축하거나, 로그 데이터를 집계하거나, 수백만 개의 레코드에서 단어 빈도를 계산할 때, 이러한 가드 절이 누적됩니다. 개발자로서의 작업 속도를 늦추고, 코드 리뷰를 어렵게 만들며, 한 분기에서 검사를 잊으면 미묘한 버그를 유발합니다.

Python의 collections.defaultdict는 이러한 문제 범주 전체를 제거합니다. 누락된 값을 자동으로 제공하기 위해 팩토리 함수를 호출하는 딕셔너리 서브클래스입니다. 더 이상 KeyError도, 가드 절도, 보일러플레이트도 필요 없습니다.

📚

defaultdict란?

defaultdict는 Python 내장 dict의 서브클래스입니다. 핵심 차이점: 존재하지 않는 키에 접근할 때, defaultdictKeyError를 발생시키는 대신 기본값으로 자동 생성합니다.

from collections import defaultdict
 
# 일반 dict는 KeyError를 발생시킴
regular = {}
# regular['missing']  # KeyError: 'missing'
 
# defaultdict는 자동으로 값을 생성함
dd = defaultdict(int)
dd['missing']  # 0을 반환하고, 이제 'missing'이 키가 됨
print(dd)  # defaultdict(<class 'int'>, {'missing': 0})

생성자는 첫 번째 인수로 팩토리 함수를 받습니다. 일반적인 팩토리:

  • int -- 0을 반환
  • list -- []를 반환
  • set -- set()을 반환
  • str -- ""를 반환
  • lambda: value -- 임의의 사용자 정의 기본값을 반환

defaultdict(int) -- 카운팅 패턴

가장 일반적인 사용법입니다. 모든 새 키가 0에서 시작하므로 즉시 증가시킬 수 있습니다.

from collections import defaultdict
 
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
 
# defaultdict 없이
counts_regular = {}
for word in words:
    if word in counts_regular:
        counts_regular[word] += 1
    else:
        counts_regular[word] = 1
 
# defaultdict(int) 사용 -- 깔끔하고 직접적
counts = defaultdict(int)
for word in words:
    counts[word] += 1
 
print(dict(counts))
# {'apple': 3, 'banana': 2, 'cherry': 1}

defaultdict(list) -- 그룹화 패턴

관련된 항목을 함께 그룹화합니다. 각 새 키는 빈 리스트로 시작합니다.

from collections import defaultdict
 
students = [
    ('Math', 'Alice'),
    ('Science', 'Bob'),
    ('Math', 'Charlie'),
    ('Science', 'Diana'),
    ('Math', 'Eve'),
    ('History', 'Frank'),
]
 
groups = defaultdict(list)
for subject, student in students:
    groups[subject].append(student)
 
for subject, names in groups.items():
    print(f"{subject}: {', '.join(names)}")
 
# Math: Alice, Charlie, Eve
# Science: Bob, Diana
# History: Frank

여러 필드로 레코드 그룹화

from collections import defaultdict
 
sales = [
    {'region': 'East', 'product': 'Widget', 'amount': 100},
    {'region': 'West', 'product': 'Gadget', 'amount': 200},
    {'region': 'East', 'product': 'Widget', 'amount': 150},
    {'region': 'West', 'product': 'Widget', 'amount': 300},
]
 
by_region_product = defaultdict(list)
for sale in sales:
    key = (sale['region'], sale['product'])
    by_region_product[key].append(sale['amount'])
 
for (region, product), amounts in by_region_product.items():
    total = sum(amounts)
    print(f"{region} - {product}: {amounts} (total: {total})")

defaultdict(set) -- 고유 그룹화

키별로 고유한 값을 자동으로 수집합니다.

from collections import defaultdict
 
edges = [
    ('Alice', 'Bob'), ('Alice', 'Charlie'),
    ('Bob', 'Alice'), ('Bob', 'Diana'),
    ('Alice', 'Bob'),  # duplicate
]
 
connections = defaultdict(set)
for person, friend in edges:
    connections[person].add(friend)
 
for person, friends in connections.items():
    print(f"{person} is connected to: {friends}")
# Alice is connected to: {'Bob', 'Charlie'}
# Bob is connected to: {'Alice', 'Diana'}

defaultdict(lambda: value) -- 사용자 정의 기본값

내장 타입이 맞지 않을 때, lambda를 사용하여 임의의 기본값을 반환합니다.

from collections import defaultdict
 
# 누락된 항목에 대한 기본값 'N/A'
status = defaultdict(lambda: 'N/A')
status['server1'] = 'running'
status['server2'] = 'stopped'
print(status['server3'])   # N/A
 
# 기본 시작 잔액
accounts = defaultdict(lambda: 100.0)
accounts['alice'] += 50
accounts['bob'] -= 30
print(dict(accounts))  # {'alice': 150.0, 'bob': 70.0}

구조화된 값을 가진 기본 딕셔너리

from collections import defaultdict
 
def default_profile():
    return {'score': 0, 'level': 1, 'items': []}
 
profiles = defaultdict(default_profile)
profiles['player1']['score'] += 100
profiles['player1']['items'].append('sword')
profiles['player2']['level'] = 5
 
print(profiles['player1'])
# {'score': 100, 'level': 1, 'items': ['sword']}
print(profiles['player3'])
# {'score': 0, 'level': 1, 'items': []}

중첩 defaultdict -- 트리 구조

가장 강력한 패턴 중 하나는 defaultdict를 재귀적으로 사용하여 자동 생성 딕셔너리를 만드는 것입니다.

from collections import defaultdict
 
def tree():
    return defaultdict(tree)
 
taxonomy = tree()
taxonomy['Animal']['Mammal']['Dog'] = 'Canis lupus familiaris'
taxonomy['Animal']['Mammal']['Cat'] = 'Felis catus'
taxonomy['Animal']['Bird']['Eagle'] = 'Aquila chrysaetos'
taxonomy['Plant']['Tree']['Oak'] = 'Quercus'
 
print(taxonomy['Animal']['Mammal']['Dog'])  # Canis lupus familiaris

다단계 집계

from collections import defaultdict
 
sales_data = [
    (2025, 'Q1', 'Widget', 500),
    (2025, 'Q1', 'Gadget', 300),
    (2025, 'Q2', 'Widget', 700),
    (2026, 'Q1', 'Widget', 600),
]
 
report = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for year, quarter, product, amount in sales_data:
    report[year][quarter][product] += amount
 
print(report[2025]['Q1']['Widget'])  # 500
print(report[2026]['Q1']['Widget'])  # 600

defaultdict vs dict.setdefault() vs get() -- 비교

기능defaultdictdict.setdefault()dict.get()
임포트 필요예 (collections)아니오아니오
키 자동 생성아니오
접근 시 dict 수정아니오
호출별 사용자 정의 기본값아니오 (글로벌 팩토리)
성능 (반복)가장 빠름느림 (메서드 호출 오버헤드)가장 빠름 (변경 없음)
최적 용도반복 축적일회성 기본값읽기 전용 폴백

각각을 사용할 시기:

  • defaultdict: 많은 반복에 걸쳐 값을 구축할 때 (카운팅, 그룹화)
  • dict.setdefault(): 특정 키에 대해 가끔 기본값이 필요할 때
  • dict.get(): 딕셔너리를 수정하지 않고 폴백으로 값을 읽을 때

defaultdict를 일반 dict로 변환

from collections import defaultdict
import json
 
def defaultdict_to_dict(d):
    """Recursively convert defaultdict to regular dict."""
    if isinstance(d, defaultdict):
        d = {k: defaultdict_to_dict(v) for k, v in d.items()}
    return d
 
nested = defaultdict(lambda: defaultdict(int))
nested['x']['y'] = 10
nested['a']['b'] = 20
 
regular = defaultdict_to_dict(nested)
print(json.dumps(regular))  # {"x": {"y": 10}, "a": {"b": 20}}

기본 팩토리를 None으로 설정하여 비활성화할 수도 있습니다:

dd = defaultdict(int)
dd['a'] += 1
dd.default_factory = None
# dd['missing']  # 이제 KeyError 발생

실용적인 예제

그래프의 인접 리스트

from collections import defaultdict, deque
 
edges = [('A', 'B'), ('A', 'C'), ('B', 'D'), ('C', 'D'), ('D', 'E')]
 
graph = defaultdict(list)
for src, dst in edges:
    graph[src].append(dst)
    graph[dst].append(src)  # undirected graph
 
def bfs(graph, start):
    visited = set()
    queue = deque([start])
    order = []
    while queue:
        node = queue.popleft()
        if node not in visited:
            visited.add(node)
            order.append(node)
            queue.extend(graph[node])
    return order
 
print(bfs(graph, 'A'))  # ['A', 'B', 'C', 'D', 'E']

텍스트 검색용 역 인덱스

from collections import defaultdict
 
documents = {
    'doc1': 'python is a great programming language',
    'doc2': 'data science uses python extensively',
    'doc3': 'machine learning with python and data',
}
 
index = defaultdict(set)
for doc_id, text in documents.items():
    for word in text.split():
        index[word.lower()].add(doc_id)
 
def search(query):
    return index.get(query.lower(), set())
 
print(search('python'))  # {'doc1', 'doc2', 'doc3'}
print(search('data'))    # {'doc2', 'doc3'}

PyGWalker로 그룹화된 데이터 시각화

defaultdict로 데이터를 그룹화하고 집계한 후, 결과를 시각화하고 싶은 경우가 많습니다. PyGWalker (opens in a new tab)는 pandas DataFrame을 Jupyter에서 직접 인터랙티브 시각화 인터페이스로 변환합니다:

from collections import defaultdict
import pandas as pd
import pygwalker as pyg
 
sales = [
    ('Electronics', 'Laptop', 1200),
    ('Electronics', 'Phone', 800),
    ('Clothing', 'Shirt', 45),
    ('Clothing', 'Jacket', 120),
]
 
totals = defaultdict(lambda: defaultdict(int))
for category, product, amount in sales:
    totals[category][product] += amount
 
rows = []
for category, products in totals.items():
    for product, total in products.items():
        rows.append({'category': category, 'product': product, 'total': total})
 
df = pd.DataFrame(rows)
walker = pyg.walk(df)

FAQ

Python에서 defaultdict란?

defaultdictcollections에 있는 딕셔너리 서브클래스로, 누락된 키에 대한 기본값을 제공합니다. KeyError를 발생시키는 대신, 팩토리 함수(int, list, set 등)를 호출하여 자동으로 기본값을 생성하고 저장합니다.

dict와 defaultdict의 차이점은?

유일한 기능적 차이는 누락된 키를 처리하는 방식입니다. 일반 dictKeyError를 발생시킵니다. defaultdictdefault_factory 함수를 호출하여 기본값을 생성합니다. 그 외 모든 면에서 동일하게 동작합니다.

defaultdict(list)와 defaultdict(set)는 언제 사용해야 하나요?

항목을 그룹화하고 중복과 삽입 순서를 보존하려면 defaultdict(list)를 사용하세요. 키별로 고유한 항목만 수집하려면 defaultdict(set)를 사용하세요.

defaultdict를 JSON으로 직렬화할 수 있나요?

예, 하지만 중첩된 defaultdict 객체의 경우 재귀적 변환 함수를 사용하여 먼저 일반 dict로 변환하세요. 직렬화 전에 우발적인 키 생성을 방지하기 위해 default_factory = None을 설정할 수도 있습니다.

중첩된 defaultdict는 어떻게 만드나요?

재귀적 팩토리 함수를 정의합니다: def tree(): return defaultdict(tree). 더 간단한 2단계 중첩에는 defaultdict(lambda: defaultdict(int))를 사용하세요.

결론

Python의 collections.defaultdict는 표준 라이브러리에서 가장 실용적인 도구 중 하나입니다. 장황하고 오류가 발생하기 쉬운 딕셔너리 축적 패턴을 깔끔한 한 줄로 변환합니다. 카운팅에는 defaultdict(int), 그룹화에는 defaultdict(list), 고유 컬렉션에는 defaultdict(set), 계층적 데이터에는 중첩 defaultdict를 사용하세요.

핵심 교훈: 딕셔너리 연산 전마다 if key not in dict를 작성하고 있다면, 그 딕셔너리를 defaultdict로 교체하세요. 코드가 더 짧아지고, 더 빨라지고, 훨씬 더 유지보수하기 쉬워집니다.

📚