[Python] dict: 해시 맵과 삽입 순서 보장
python dict, 파이썬 딕셔너리, dictionary, hash map, ordered dict
정의
dict는 Python의 해시 맵 자료구조다. Python 3.7부터 삽입 순서를 언어 사양으로 보장한다(이전엔 CPython 3.6 구현 디테일이었음). 평균 O(1) 조회·삽입·삭제. 키는 해시 가능해야 한다(tuple/str/frozenset O, list/dict/set X).
생성
empty = {}
d1 = {"a": 1, "b": 2}
d2 = dict(a=1, b=2) # 키워드 인수 (str 키만)
d3 = dict([("a", 1), ("b", 2)]) # 튜플 시퀀스
d4 = dict.fromkeys(["a", "b", "c"], 0) # {'a': 0, 'b': 0, 'c': 0}
# 컴프리헨션
squares = {x: x ** 2 for x in range(5)}
기본 연산
python
d = {"a": 1, "b": 2, "c": 3}
# 조회
print(d["a"]) # KeyError if missing
print(d.get("z")) # None if missing
print(d.get("z", -1)) # default
print("a" in d) # 멤버십
# 변경
d["d"] = 4 # 추가/덮어쓰기
d.update({"e": 5, "a": 99})
print(d)
# 삭제
del d["a"]
print(d.pop("b")) # 반환 후 제거
print(d.popitem()) # 마지막 항목 (LIFO, 3.7+)
print(d) 결과
1
None
-1
{'a': 99, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
2
('e', 5)
{'c': 3, 'd': 4}뷰: keys / values / items
d = {"a": 1, "b": 2}
d.keys() # dict_keys(['a', 'b'])
d.values() # dict_values([1, 2])
d.items() # dict_items([('a', 1), ('b', 2)])
중요: 뷰는 동적이다. dict가 바뀌면 뷰도 즉시 반영된다.
python
d = {"a": 1, "b": 2}
keys = d.keys()
print(list(keys))
d["c"] = 3
print(list(keys)) # 뷰가 업데이트됨 결과
['a', 'b']
['a', 'b', 'c']순회
d = {"a": 1, "b": 2, "c": 3}
for k in d: # 키 순회 (기본)
print(k)
for k, v in d.items(): # 키-값 동시
print(k, v)
for v in d.values():
print(v)
함정: 순회 중 dict를 변경하면 RuntimeError: dictionary changed size during iteration.
# WRONG
for k in d:
if d[k] < 0:
del d[k] # RuntimeError
# CORRECT: 키 리스트로 복사
for k in list(d.keys()):
if d[k] < 0:
del d[k]
# 또는 컴프리헨션으로 새 dict 생성
d = {k: v for k, v in d.items() if v >= 0}
defaultdict / Counter
표준 라이브러리 collections의 자주 쓰이는 dict 서브클래스.
python
from collections import defaultdict, Counter
# 그룹핑
words = ["apple", "banana", "cherry", "avocado", "blueberry"]
grouped = defaultdict(list)
for w in words:
grouped[w[0]].append(w)
print(dict(grouped))
# 빈도 계산
text = "abracadabra"
c = Counter(text)
print(c.most_common(3)) 결과
{'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
[('a', 5), ('b', 2), ('r', 2)]setdefault와 get
get(k, default)는 dict를 변경하지 않고 기본값 반환. setdefault(k, default)는 키가 없으면 추가까지.
d = {}
v = d.setdefault("count", 0) # d = {"count": 0}, v = 0
d["count"] += 1 # d = {"count": 1}
# 두 줄 = defaultdict(int)["count"] += 1 한 줄
dict 병합 (3.9+)
PEP 584 도입.
a = {"x": 1, "y": 2}
b = {"y": 99, "z": 3}
merged = a | b # {'x': 1, 'y': 99, 'z': 3}
a |= b # a 직접 갱신
# 3.9 이전:
merged = {**a, **b}
해시 가능성
키는 __hash__를 구현해야 한다. 가변 객체(list, dict, set)는 해시 불가능.
{[1, 2]: "x"} # TypeError: unhashable type: 'list'
{(1, 2): "x"} # OK
{frozenset([1, 2]): "x"} # OK
해시값과 동등성은 일관되어야 한다(a == b → hash(a) == hash(b)). 직접 만든 클래스를 키로 쓰려면 __eq__와 __hash__를 함께 구현.
내부 구조 간단히
- Open addressing + perturbation probing
- 3.6+ compact dict: 해시 테이블에는 인덱스만, 실제 항목은 별도 배열에 순서대로 저장 → 메모리 절약 + 순서 보존
- load factor 약 2/3 도달 시 리사이즈
import sys
sys.getsizeof({}) # 64 (3.12 기준)
sys.getsizeof({"a": 1}) # 184
성능 함정
- 매우 큰 dict에서 키 정렬이 필요하면
sorted(d)매번 호출보다sortedcontainers.SortedDict검토 - 키 충돌이 심한 사용자 정의 객체는 O(n) 가능 →
__hash__잘 분포되게 구현 dict대신__slots__+ 클래스: 메모리 1/3, 속도 약간 빠름 (필드 고정 시)
💬 댓글