파이썬/기초 프로그래밍

파이썬 해시테이블 개념 및 튜플 → 딕셔너리 변환하기

Data Jun 2025. 9. 7. 15:55

파이썬의 **딕셔너리(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로 깔끔하게 정리 가능