파이썬의 **딕셔너리(dict)**는 내부적으로 **해시테이블(Hash Table)**이라는 구조를 사용합니다.
딕셔너리는 key: value 형태로 데이터를 저장하고, 키를 이용해 값을 아주 빠르게 찾을 수 있어요.
1. 해시테이블이란?
해시테이블(Hash Table)은 데이터를 (Key, Value) 쌍으로 저장하는 자료구조예요.
- Key → 해시 함수(Hash function)를 거쳐 인덱스로 변환
- Value → 해당 위치에 저장
파이썬의 딕셔너리(dict) 가 바로 해시테이블을 이용한 대표적인 자료구조입니다.
2. 해시란?
딕셔너리(해시테이블)에서 **해시(hash)**는 보통 **키를 해시 함수에 넣어 얻는 정수값(해시값)**을 말합니다. 이 해시값은 저장 위치를 바로 가리키는 주소가 아니라, 어디에 있을지 빠르게 좁혀 주는 숫자 힌트입니다.
- 해시값이 하는 일 ✅
- hash(key)로 얻은 정수로 버킷 인덱스를 계산합니다(예: hash(key) % 테이블크기).
- 후보 버킷을 빠르게 찾고, 마지막에 실제 키를 다시 비교해 정확한 항목을 결정합니다.
- 해시값이 아닌 것 ❌
- 키(Key) 자체가 아닙니다(키는 PK처럼 유일).
- **값(Value)**도 아닙니다(키에 매핑된 실제 데이터).
- 메모리 주소도 아닙니다(객체의 실제 위치).
# 동작 흐름 예시
key = "apple"
h = hash(key) # 1) 해시값(정수)
index = h % 8 # 2) 버킷 인덱스 계산(테이블 크기 = 8)
bucket = table[index] # 3) 후보 버킷 접근
# 4) bucket 내부에서 실제 key == "apple" 비교로 최종 항목을 찾고 Value 읽기/갱신
- **충돌(중복 해시값)**은 정상입니다. 서로 다른 키가 같은 해시값을 가질 수 있지만,
버킷 안에서 키를 다시 비교하므로 올바른 값을 정확히 찾을 수 있습니다. - 반대로 키는 유일해야 합니다(같은 키를 다시 넣으면 기존 값이 덮어쓰기).
해시는 **키에서 계산된 ‘찾기용 숫자 힌트(해시값)’**입니다. 해시값은 중복 가능하고, 최종 판정은 키 비교로 합니다. 키는 유일 식별자, 값은 실제 데이터, 해시값은 인덱싱 힌트입니다.
3. 해시 가능한 객체란?
딕셔너리에서 키(key) 로 쓰려면 반드시 해시 가능한 객체여야 합니다.
여기서 "해시 가능"이란 값이 변하지 않고, hash() 값이 유지되는 객체를 말해요.
- 해시 가능 ✅: 정수(int), 문자열(str), 불변 튜플(tuple)
- 해시 불가 ❌: 리스트(list), 딕셔너리(dict) → 가변이므로 hash값이 변할 수 있음
t1 = (10, 20, (30, 40, 50)) # 튜플 안에 튜플만 있음 → 불변 → OK
t2 = (10, 20, [30, 40, 50]) # 튜플 안에 리스트 있음 → 가변 → ERROR
print(hash(t1)) # 정상 동작
# print(hash(t2)) # TypeError 발생
튜플은 가능하지만, 리스트는 불가능합니다.
4. 그럼 딕셔너리는 해시테이블인데, 왜 해시 가능하지 않을까?
여기서 많이 헷갈립니다.
- 딕셔너리(dict) 자체는 "해시테이블을 이용한 자료구조"예요. 즉, 내부적으로 해시를 사용합니다.
- 하지만 딕셔너리 객체 그 자체는 가변(mutable)이기 때문에, 다른 딕셔너리의 키로 쓸 수는 없습니다.
- 딕셔너리 = 해시테이블을 구현한 자료구조
- 딕셔너리 객체 = 가변이므로 해시 불가 (hash() 불가능, 키로 못 씀)
즉, “해시를 이용해 동작하지만, 자기 자신은 해시 가능한 객체가 아니다” 라고 이해하면 됩니다.
5. 튜플 모음을 딕셔너리로 변환하기
아래처럼 (key, value) 형태의 데이터가 있다고 해봅시다.
source = (
('k1', 'val1'),
('k1', 'val2'),
('k2', 'val3'),
('k2', 'val4'),
('k2', 'val5')
)
이 데이터를
{'k1': ['val1', 'val2'], 'k2': ['val3', 'val4', 'val5']}
이렇게 바꾸고 싶다면?
1️⃣ 방법 1: if문으로 처리하기
new_dict1 = {}
for k, v in source:
if k in new_dict1: # 이미 키가 있으면
new_dict1[k].append(v) # 값 추가
else: # 없으면 새 리스트 생성
new_dict1[k] = [v]
print(new_dict1)
# {'k1': ['val1', 'val2'], 'k2': ['val3', 'val4', 'val5']}
2️⃣ 방법 2: setdefault로 더 간단하게
new_dict2 = {}
for k, v in source:
new_dict2.setdefault(k, []).append(v)
print(new_dict2)
# {'k1': ['val1', 'val2'], 'k2': ['val3', 'val4', 'val5']}
- setdefault(key, default) → 키가 없으면 default를 넣고 반환
- 따라서 .append()를 바로 붙여도 에러가 나지 않습니다.
정리하면
- 해시테이블: Key → 해시 함수 → Value를 저장하는 구조 (딕셔너리의 원리)
- 해시 가능한 객체: 불변이라 hash()값이 일정한 객체 (int, str, tuple …)
- 딕셔너리: 해시테이블 기반 구조이지만 자체는 가변 → 해시 불가 (키로 사용 불가)
- 튜플 변환: setdefault()나 defaultdict로 깔끔하게 정리 가능
'파이썬 > 기초 프로그래밍' 카테고리의 다른 글
| 파이썬 reduce로 배우는 누적 연산 (0) | 2025.09.07 |
|---|---|
| 파이썬에서 딕셔너리를 읽기 전용으로 만들기 — MappingProxyType (0) | 2025.09.07 |
| 파이썬 sorted() vs 리스트 .sort() 제대로 정리하기 (0) | 2025.09.07 |
| 파이썬의 시퀀스형(Sequence) (0) | 2025.09.07 |
| 파이썬 리스트 곱하기(*)와 참조 공유 문제 (0) | 2025.09.07 |