본문으로 건너뛰기
김신건의 로그

[Python] dict: 해시 맵과 삽입 순서 보장

· 수정 · 📖 약 1분 · 402자/단어 #python #dict #dictionary #hash #basics
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 == bhash(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, 속도 약간 빠름 (필드 고정 시)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기