class Key:
def __init__(self, val):
self.val = val
def __eq__(self, other):
return self.val == other.val
def __hash__(self):
return hash(self.val)
k1 = Key(10)
k2 = Key(10)
k3 = Key(20)
d = {}
d[k1] = 'apple'
d[k2] = 'banana'
d[k3] = 'carrot'
print(len(d))
print(d[k1])
class Key:Key라는 사용자 정의 클래스 선언Key를 메모리에 등록한다.Key라는 이름으로 새로운 자료형이 만들어진 상태def __init__(self, val): ... self.val = valval 값을 멤버변수 self.val에 저장Key(10) 하면 이 메서드가 호출되고 val=10이 self.val에 저장됨k1 = Key(10) → k1.val == 10def __eq__(self, other): return self.val == other.valKey 객체가 == 비교될 때 멤버 val 값으로 비교==가 기본 객체 ID 비교가 아니라 이 val 값 비교가 되도록 해야 동일 키로 인식k1 == k2 호출하면 k1.val == k2.val 값 비교 결과 반환k1.val=10, k2.val=10 → k1 == k2 는 Truedef __hash__(self): return hash(self.val)Key 객체의 해시값을 멤버변수 val의 해시값으로 반환hash()로 위치 탐색 → val 값이 같으면 같은 해시값을 가지도록 해야 키 충돌 가능hash(k1)는 hash(10)과 동일한 값을 반환hash(k1) == hash(k2)는 True (둘 다 10으로부터 해시 계산)k1 = Key(10), k2 = Key(10), k3 = Key(20)역할: Key 객체 세 개 생성, 각각 val 10, 10, 20
왜? 예제를 통해 같은 값이지만 다른 객체인 경우와 다른 값을 가진 객체를 비교
실행 시: k1.val=10, k2.val=10, k3.val=20
시각화:
k1 -> Key(val=10)
k2 -> Key(val=10)
k3 -> Key(val=20)
d = {}d = {} (빈 상태)d[k1] = 'apple'역할: 딕셔너리에 k1을 키로, 'apple'을 값으로 저장
동작 원리:
hash(k1) 호출 → hash(10)hash(10) 위치 찾음k1 키와 'apple' 값 저장상태:
d = {
k1: 'apple'
}
d[k2] = 'banana'역할: 딕셔너리에 k2를 키로, 'banana'를 값으로 저장
핵심 동작:
hash(k2) 호출 → hash(10) (k1과 동일)k1이 있음 → 키가 같은지 비교k2 == k1 비교 → k2.val == k1.val (10 == 10) → Truek1의 값 'apple'을 'banana'로 덮어씀상태:
d = {
k1: 'banana' # 기존 apple 덮어쓰기됨
}
중요한 점: 키는 k1과 k2가 같은 것으로 간주돼 하나만 존재
d[k3] = 'carrot'역할: 딕셔너리에 k3 키, 'carrot' 값 저장
동작:
hash(k3) 호출 → hash(20)상태:
d = {
k1: 'banana',
k3: 'carrot'
}
print(len(d))d의 요소 개수 출력k1과 k3 두 개2print(d[k1])k1에 대응하는 값을 출력k1 키의 값은 'banana' (15번 줄에서 덮어쓰기됨)'banana'len(d) → 2d[k1] → 'banana'즉, 정답은 B번
k1과 k2는 별개 객체지만, __hash__()와 __eq__() 오버라이드 덕분에 동일한 키로 취급됨d[k2] = 'banana'는 d[k1]의 값을 덮어쓰는 것과 같음k1/k2 하나 묶임, k3 하나)만 가짐좋아, 이번에도 풀이 없이 문제만 주는 건 없다!
이제부터 위 코드 한 줄 한 줄을 완전히 디버깅하듯 해설해줄게. 이 문제의 핵심은 **mutable 객체(리스트)를 dict의 키로 쓰는 것이 가능한가?**에 대한 트랩이야.
a = [1, 2, 3]
b = [1, 2, 3]
d = {}
d[a] = "alpha"
d[b] = "beta"
print(len(d))
a = [1, 2, 3]역할: 리스트 [1, 2, 3]를 생성하고 변수 a에 할당
현재 상태:
a → [1, 2, 3]
b = [1, 2, 3]역할: 동일한 값을 가지는 새로운 리스트를 만들고 변수 b에 할당
a와 b는 값은 같지만, 서로 다른 객체
현재 상태:
b → [1, 2, 3]
a is b → False
a == b → True
✅
==는 내부 값 비교
❌is는 객체 주소(동일 객체) 비교
d = {}역할: 빈 딕셔너리 생성
이 딕셔너리는 key-value 쌍을 저장하는 해시 테이블
상태:
d = {}
d[a] = "alpha"a를 key로 사용해 "alpha" 저장 시도__hash__()가 정의되어 있지 않음>>> hash([1,2,3])
TypeError: unhashable type: 'list'
dict의 키로 사용 불가d[a] = "alpha"
에서 **TypeError: unhashable type: 'list'**가 발생하면서 프로그램 종료됨
d[b] = "beta"는 실행되지 않음print(len(d))도 도달하지 않음| 조건 | 설명 |
|---|---|
| 불변(immutable) | 값을 변경할 수 없어야 함 (예: int, str, tuple) |
| 해시 가능 (hashable) | __hash__()가 정의돼 있고, __eq__() 비교와 일관성이 있어야 함 |
| 리스트(list), 딕셔너리(dict), 집합(set) 등은 mutable → 해시 불가능 |
아래처럼 tuple을 key로 쓰면 동작함 (immutable이라서):
a = (1, 2, 3)
b = (1, 2, 3)
d = {}
d[a] = "alpha"
d[b] = "beta"
print(len(d)) # 출력: 1
→ 이 경우 a == b이고 hash(a) == hash(b)이므로 같은 키로 간주되어 덮어쓰기됨
아주 좋아! 이 코드는 해시와 불변(immutable) 객체의 핵심 동작 원리를 묻는 대표적인 예제야.
지금부터 각 줄을 디버깅 수준으로 해부해서 왜 len(d)가 1이 되는지 완벽하게 설명해줄게.
a = (1, 2, 3)
b = (1, 2, 3)
d = {}
d[a] = "alpha"
d[b] = "beta"
print(len(d)) # 출력: 1
a == b → 값 비교 결과 Truehash(a) == hash(b) → 튜플의 값이 같으면 해시도 같음b를 key로 넣으면 a 키와 동일하다고 판단되어 덮어쓰기 발생a = (1, 2, 3)역할: 튜플 (1, 2, 3)을 변수 a에 저장
이유: 딕셔너리 키로 쓸 수 있도록 불변(immutable) 객체 사용
상태:
a → (1, 2, 3)
b = (1, 2, 3)역할: 같은 값을 가진 튜플을 새로 하나 더 생성해서 b에 저장
주의: a와 b는 서로 다른 객체지만 같은 값을 가짐
확인:
a == b → True # 값 비교 (튜플은 요소 하나씩 비교함)
a is b → False # 객체 주소 비교 (다름)
시각화:
메모리
┌──────┐ ┌──────┐
│ a │ │ b │
│(1,2,3)│ │(1,2,3)│
└──────┘ └──────┘
d = {}역할: 빈 딕셔너리 생성
상태:
d = {}
d[a] = "alpha"동작:
hash(a) 호출 → hash((1, 2, 3))a와 값 "alpha" 저장상태:
d = {
(1, 2, 3): "alpha"
}
d[b] = "beta"이 부분이 핵심이다. 지금부터 이 줄에서 정확히 어떤 일이 벌어지는가를 디버깅처럼 따라가 보자.
b를 딕셔너리 키로 넣으려면 해시값을 구함hash(b) = hash((1, 2, 3))a와 동일함== 비교로 같은 키인지 판단b == a → True파이썬은 이미 a라는 키가 존재하고,
b도 동일한 키로 인식되기 때문에
"alpha"를 "beta"로 덮어쓰기한다.
최종 상태:
d = {
(1, 2, 3): "beta"
}
print(len(d))11| 개념 | 설명 |
|---|---|
불변 자료형 (tuple, str, int) | hash() 가능, 딕셔너리 key로 사용 가능 |
| 값이 같으면 | ==와 hash() 모두 같으면 key로 덮어쓰기됨 |
| 파이썬 딕셔너리 key 비교 방식 | 1️⃣ hash(key) → 같으면 → 2️⃣ key1 == key2 비교까지 수행 |
a = (1, 2, 3)
b = (1, 2, 3)
print(hash(a)) # ex) 2528502973977326415
print(hash(b)) # 동일한 값 나옴
print(a == b) # True
print(a is b) # False (다른 객체)
d = {}
d[a] = "apple"
d[b] = "banana"
print(d) # {(1, 2, 3): 'banana'}
print(len(d)) # 1