[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).
내부 구조 (compact dict, 3.6+)
flowchart TB
subgraph HT["해시 테이블 (indices 배열)"]
H0["slot 0: -"]
H1["slot 1: 2"]
H2["slot 2: 0"]
H3["slot 3: -"]
H4["slot 4: 1"]
end
subgraph EA["entries 배열 (삽입 순서)"]
E0["0: hash=..., key='a', val=1"]
E1["1: hash=..., key='b', val=2"]
E2["2: hash=..., key='c', val=3"]
end
H2 --> E0
H4 --> E1
H1 --> E2
- 해시 테이블: 슬롯마다 entries 배열의 인덱스만 저장
- entries 배열: 실제 (hash, key, value) 를 삽입 순서대로 저장
- 삽입 순서 보장 + 메모리 절약 동시 달성 (이전 CPython 대비 ~50% 절약)
생성
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
True
{'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)
WARNING
순회 중 변경 금지: 순회 중 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)]Counter 산술
from collections import Counter
a = Counter(["cat", "dog", "cat"])
b = Counter(["dog", "bird"])
print(a + b) # Counter({'cat': 2, 'dog': 2, 'bird': 1})
print(a - b) # Counter({'cat': 2})
print(a & b) # Counter({'dog': 1}) # min(a, b)
print(a | b) # Counter({'cat': 2, 'dog': 1, 'bird': 1}) # max(a, b)
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}
ChainMap: 계층적 조회
collections.ChainMap 은 여러 dict를 체인으로 연결. 앞에서부터 순서대로 키 검색.
from collections import ChainMap
defaults = {"color": "red", "verbose": False}
env = {"verbose": True}
cli = {"output": "json"}
config = ChainMap(cli, env, defaults)
print(config["color"]) # "red" (defaults 에서)
print(config["verbose"]) # True (env 에서 오버라이드)
print(config["output"]) # "json" (cli 에서)
# 실전: argparse + 환경변수 + 기본값 계층
해시 가능성
키는 __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__를 함께 구현.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # tuple 해시 재활용
d = {Point(0, 0): "origin"}
내부 구조 간단히
- 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를 클래스처럼: TypedDict
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "Alice", "age": 30}
# 정적 타입 검사 지원, 런타임엔 일반 dict
조건부 키 포함
# 3.9+ 전: 조건 키 추가하려면 두 번 작성
config = {}
if debug:
config["verbose"] = True
# 3.9+ 병합 연산자 활용
config = {"host": "localhost"} | ({"verbose": True} if debug else {})
함수형: dict → 변환
inventory = {"apple": 5, "banana": 0, "cherry": 3}
# 0 제거
available = {k: v for k, v in inventory.items() if v > 0}
# 값 변환
doubled = {k: v * 2 for k, v in inventory.items()}
# 키 정렬
sorted_inv = dict(sorted(inventory.items(), key=lambda kv: kv[1], reverse=True))
성능 함정
CAUTION
매우 큰 dict에서 키 정렬이 필요하면 sorted(d) 매번 호출보다 sortedcontainers.SortedDict 검토.
- 키 충돌이 심한 사용자 정의 객체는 O(n) 가능 →
__hash__잘 분포되게 구현 dict대신__slots__+ 클래스: 메모리 1/3, 속도 약간 빠름 (필드 고정 시)dict.get()vstry/except KeyError: 키 존재 확률 낮으면 예외 방식이 더 빠름
관련 위키
- py-comprehension - dict comprehension 문법
- py-set-frozenset - set: 해시 가능 키 집합
- py-collections - defaultdict, Counter, ChainMap, OrderedDict
- py-typing - TypedDict, Mapping, MutableMapping 타입 힌트
- py-dataclass - 구조화 데이터: dict 대신 클래스 쓸 때
이 글의 용어 (5개)
- [Python] collections: Counter, deque, defaultdict, OrderedDict, ChainMappython
- 정의 모듈은 dict/list/tuple/set의 특수화 컨테이너를 제공한다. 표준 라이브러리에 있는 데이터 구조 도구함의 핵심. | 클래스 | 용도 | |--------|---…
- [Python] Comprehension: list, dict, set, generatorpython
- 정의 Comprehension(컴프리헨션)은 iterable로부터 list/set/dict/generator를 선언적·간결하게 만드는 문법이다. 일반 + 보다 30-50% 빠르며…
- [Python] dataclass: 자동 생성 메서드를 갖춘 데이터 클래스python
- 정의 (3.7+, PEP 557)는 클래스에 , , 등 상용구 메서드를 자동 생성해주는 데코레이터다. 데이터 컨테이너 클래스를 한 줄 데코레이터 + 필드 어노테이션만으로 만들 수…
- [Python] set, frozensetpython
- 정의 은 중복 없는 해시 가능 원소의 가변 컬렉션이다. dict의 키 슬롯만 가진 형태로 구현되어 평균 O(1) 멤버십·삽입·삭제를 제공한다. 은 그 불변 버전으로 해시 가능하다…
- [Python] typing: 타입 힌트 기초python
- 정의 Python 타입 힌트(PEP 484)는 런타임에 강제되지 않는 정적 어노테이션이다. , , 같은 타입 체커가 정적 분석에 사용. 런타임은 어노테이션을 무시 (단, 일부 프…
💬 댓글