좋아! 이번엔 완전히 다른 스타일로 문제를 낼게.
이번 문제는 실전 실기 문제 스타일 + 코딩 트랩 + 해시 내부 원리가 섞인 응용 문제야.
이번엔 dict.get()과 기본값 처리, 해시 키 갱신 문제, mutable 객체 값 증가 트랩이 섞여 있어.
다음 코드를 실행했을 때 출력 결과로 올바른 것을 고르시오.
data = ["apple", "banana", "apple", "orange", "banana", "apple"]
counter = {}
for item in data:
counter.get(item, 0)
counter[item] = counter.get(item, 0) + 1
print(counter["apple"])
print(counter["banana"])
print(counter["orange"])
print(len(counter))
A.
3
2
1
3
B.
1
1
1
3
C.
3
2
1
6
D.
3
2
1
2
.get(key, default)는 무슨 역할을 하지?counter[item] = counter.get(item, 0) + 1은 몇 번 실행될까?data = ["apple", "banana", "apple", "orange", "banana", "apple"]과일 이름이 담긴 리스트
총 6개 요소 중:
counter = {}for item in data:
counter.get(item, 0)
counter[item] = counter.get(item, 0) + 1
counter.get(item, 0)은 첫 줄에서 아무것도 안 함counter[item] = counter.get(item, 0) + 1
동작:
item 키가 딕셔너리에 있으면 그 값을 가져와서 +1| item | 실행 내용 | counter 상태 |
|---|---|---|
"apple" | counter["apple"] = 0 + 1 | {"apple": 1} |
"banana" | counter["banana"] = 0 + 1 | {"apple": 1, "banana": 1} |
"apple" | counter["apple"] = 1 + 1 | {"apple": 2, "banana": 1} |
"orange" | counter["orange"] = 0 + 1 | {"apple": 2, "banana": 1, "orange": 1} |
"banana" | counter["banana"] = 1 + 1 | {"apple": 2, "banana": 2, "orange": 1} |
"apple" | counter["apple"] = 2 + 1 | {"apple": 3, "banana": 2, "orange": 1} |
print(counter["apple"]) # 3
print(counter["banana"]) # 2
print(counter["orange"]) # 1
print(len(counter)) # 3 (apple, banana, orange)
3
2
1
3
dict.get(key, default)는 key가 없을 때 default 값을 반환 (하지만 딕셔너리에 추가하지 않음!)counter[item] = counter.get(item, 0) + 1 은 key가 없을 땐 0으로 시작해서 count uplen(counter)는 최종적으로 들어간 key 개수get()만 호출해도 키가 생긴다고 착각하는 경우!counter.get("apple", 0) # 단순히 0을 반환할 뿐, 딕셔너리에 key 추가 안 됨!
아래처럼 해야 실제로 key가 생김:
counter["apple"] = counter.get("apple", 0) + 1
좋아! 이번엔 진짜 실무 스타일 해시 문제 + 객체의 __eq__, __hash__ 오버라이딩 실수 트랩이 숨어 있는 유형을 낼게.
사용자 정의 객체를 key로 쓸 때 벌어지는 치명적인 실수를 담고 있어. 정보처리기사 실기나 기업 코딩 테스트에서 실전 트랩으로 자주 출제되는 유형이야.
다음 Python 코드를 실행하면 출력 결과는 무엇인가?
class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
# def __hash__(self):
# return hash(self.name)
i1 = Item("pen")
i2 = Item("pen")
d = {}
d[i1] = 100
d[i2] = 200
print(len(d))
print(d[i1])
A.
1
200
B.
1
100
C.
2
100
D.
2
200
i1 == i2는 True일까?__hash__가 주석 처리돼 있을까?class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
Item 선언__eq__() 오버라이딩 → 값으로 비교 가능 (name이 같으면 같은 객체처럼 보이게 됨)__hash__()는 오버라이딩 안됨object.__hash__() 사용됨 → 각 인스턴스는 고유한 해시값을 가짐i1 = Item("pen")
i2 = Item("pen")
i1.name == i2.name == "pen"i1 == i2 → Truehash(i1) != hash(i2) → 서로 다른 해시값을 가짐💣 해시값이 다르면, dict 입장에서는 서로 다른 키로 간주!
d = {}
d[i1] = 100
d[i2] = 200
i1과 i2는 값이 같아 보여도 다른 해시값을 가지므로,상태:
d = {
i1: 100,
i2: 200
}
따라서 키 개수는 2
print(len(d)) # 2
print(d[i1]) # 100
2
100
| 항목 | 설명 |
|---|---|
__eq__()만 있고 __hash__() 없으면 | 해시 구조에서는 key가 다르게 취급됨 |
| 딕셔너리 key 조건 | hash() 같고 ==도 같아야 같은 key |
| 이 코드에서의 문제 | __eq__()로 같다고 했지만 __hash__()가 없어서 서로 다른 해시값 생성됨 |
print(i1 == i2) # True
print(hash(i1)) # 예: 875812567812
print(hash(i2)) # 예: 129301235621 (다름)
class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
그렇게 되면 i1과 i2는 같은 key로 간주돼, 덮어쓰기됨:
d = {}
d[i1] = 100
d[i2] = 200
print(len(d)) # 1
print(d[i1]) # 200
→ 이 경우 정답은 A가 됨.
좋은 질문이야! 결론부터 말하면:
**"이름(
name)을 한 번 더 가져다 쓰는__hash__()메서드가 있어야 '값이 같은 객체'를 해시 key로 인정한다"**는 말은 부분적으로 맞지만, 핵심은 다음과 같아.
파이썬에서 어떤 객체가 딕셔너리의 키(key) 로 쓰일 수 있으려면 반드시 다음 두 조건을 모두 만족해야 돼:
__hash__() 오버라이딩 → 해시값이 같아야 함name)을 가진 객체는 같은 hash() 값을 반환해야 함__eq__() 오버라이딩 → 동등 비교가 True여야 함obj1 == obj2가 True여야 같은 키로 인식됨__eq__()만 오버라이딩class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
i1 == i2 → Truehash(i1) != hash(i2)그래서 딕셔너리에서 key로 사용할 때는:
d[i1] = 100
d[i2] = 200
→ 서로 다른 키로 인식됨 → len(d) == 2
__hash__()도 같이 오버라이딩class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name) # 이 부분이 핵심!
i1 == i2 → Truehash(i1) == hash(i2) → True→ 이제 딕셔너리에서 key로 사용할 때는 같은 키로 인식돼 덮어쓰기 발생
| 케이스 | __eq__ | __hash__ | 결과 |
|---|---|---|---|
| 둘 다 없음 | 기본 비교 | 기본 해시 | 객체별 다름 (key 충돌 안 함) |
__eq__만 있음 | 값은 같다고 인식 | 해시는 다름 | 서로 다른 키 취급 (충돌 발생) |
__eq__, __hash__ 둘 다 있음 | 값 같음 | 해시 같음 | 같은 키로 인식됨 ✔️ |
name을 써야 해?name이라면, hash()도 동일 기준(name)을 써야 해==가 True면 hash()도 같아지기 때문이야✅ "값 비교 기준에 사용하는 필드를 기준으로
__hash__()도 계산해야 한다"가 더 정확한 말이야.
파이썬 문서(Data model)에서도 명시함:
❗ If a class overrides
__eq__(), it should also override__hash__()so that two equal objects have the same hash value.
✔️ "값 기준이
name이면,__hash__()도name을 기준으로 만들어야 한다"는 건 맞는 말이야.
❌ 하지만 "name을 한 번 더 불러야만 해시값이 인정된다"는 표현은 오해의 소지가 있어.
핵심은__eq__와__hash__가 같은 기준(동일 필드)을 써야 한다는 것이야!
좋아, 네 질문을 정확히 다시 정리하면 이거지:
"파이썬에서
return hash(self.name)처럼 직접hash()를 호출해서__hash__에서 리턴해야만 딕셔너리 key로 인식이 되는가?"
그리고 "이게 파이썬에서 통용되는 공식 규칙이냐?"
정확하게 답해줄게. 👇
✔️ 반드시
__hash__()메서드를 명시적으로 오버라이딩해서 정의해야만, 파이썬은 해당 객체를 딕셔너리나 set의 key로 정상적으로 인식해.❗그리고 그 안에서
return hash(self.name)처럼 불변(immutable) 값을 기준으로 해시값을 계산해야 해.
hash(self.name)처럼 직접 hash()를 호출해야 해?파이썬에서 어떤 객체를 dict나 set의 key로 쓰면 다음 두 단계를 수행해:
__hash__()를 호출해서 해시값을 계산함__eq__()로 두 객체를 비교함==까지 True여야 “이건 같은 키”로 인정하고 덮어쓰기return hash(self.name)이어야 하는가?self.name은 str 타입 → str은 immutable → 해시 가능hash(self.name) → 항상 같은 값을 반환하므로 안전name으로 보겠다는 의미야class Item:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name) # ✔️ name 기준으로 hash
이건 "이 클래스는 name이 같으면 같은 객체로 간주해줘!" 라고 명시적으로 선언한 거야.
“hash()를 안 쓰고 그냥 숫자 리턴해도 되는 거 아닌가요?”
→ 불가하진 않지만 위험해!
def __hash__(self):
return 42 # 💥 해시 충돌 유발 가능성 매우 높음
def __hash__(self):
return id(self) # ❌ 이건 기본 object hash와 같음 (주소 기반)
__eq__()가 True여도 해시가 다르면 딕셔너리에서 다른 키로 취급If a class does not define a
__hash__()method and it defines a__eq__()method, its instances will not be usable as dictionary keys or set elements.
그리고:
If you override
__eq__(), you MUST override__hash__()as well, in a way that is consistent.
__eq__)과 같은 기준으로 __hash__()도 계산해야 한다는 뜻| 질문 | 답변 |
|---|---|
return hash(self.name)처럼 써야 하나요? | ✅ 네, 그게 가장 안전하고 권장되는 방식입니다. |
왜 꼭 hash()로 감싸야 하죠? | name이 문자열이라서 해시 가능한 객체이고, 그걸 기반으로 해시값을 만들어야 비교 일관성이 생깁니다. |
| 그냥 숫자 리턴하면 안 되나요? | ❌ 가능은 하지만 성능 저하, 충돌 위험, 일관성 오류 발생 가능성이 큼 |
| 공식 규칙인가요? | ✅ __eq__()를 오버라이딩하면 반드시 __hash__()도 같이 오버라이딩해야 한다는 게 파이썬 공식 규칙입니다. |
# 잘못된 예: 해시 안 정의함
class BadItem:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = BadItem("pen")
b = BadItem("pen")
print(a == b) # ✅ True
print(hash(a)) # ❌ TypeError 발생 → unhashable
class GoodItem:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
a = GoodItem("pen")
b = GoodItem("pen")
print(a == b) # ✅ True
print(hash(a)) # ✅ OK
print(hash(b)) # ✅ OK (동일함)
좋아! 그럼 이번엔 좀 더 실무 지향적이고 흔히 실수하는 **“mutable 객체 + 해시 트랩 + frozenset”**을 활용한 문제를 낼게.
이 문제는 파이썬에서 set/dict의 key에 쓸 수 있는 자료형의 기준을 완전히 이해하고 있는지를 묻는 문제야.
다음 Python 코드의 실행 결과로 올바른 것은?
a = set([1, 2])
b = set([1, 2])
s = set()
s.add(frozenset(a))
s.add(frozenset(b))
print(len(s))
A. 1
B. 2
C. 에러 발생
D. TypeError: unhashable type: 'set'
set 안에 set을 넣을 수 있을까?frozenset은 무엇인가?a와 b는 값이 같으면 같은 키가 되나?a = set([1, 2]) / b = set([1, 2])set 객체를 만듦a == b # True
a is b # False
하지만 set은 mutable이기 때문에 hash() 불가!
hash(a) # ❌ TypeError
frozenset(a) / frozenset(b)frozenset은 immutable set이야hash() 가능! → set/dict의 키로 사용 가능hash(frozenset(a)) # ✅ OK
그리고:
frozenset(a) == frozenset(b) # ✅ True
s = set() → 빈 집합 생성s.add(frozenset(a))
s.add(frozenset(b))
frozenset(a)와 frozenset(b)는 값이 같고,==도 True이고, hash()도 같음→ 그래서 두 번째 add()는 무시됨 (이미 존재하는 원소)
print(len(s))frozenset({1, 2})가 하나만 들어감11| 개념 | 설명 |
|---|---|
set | mutable → hash 불가능 → dict/set의 key로 사용 불가 |
frozenset | immutable → hash 가능 → dict/set의 key로 사용 가능 |
set.add(x) | 이미 존재하는 원소(==와 hash() 모두 같으면) 추가 안 함 |
| 상황 | 조치 |
|---|---|
| 집합 안에 집합을 넣고 싶을 때 | frozenset 사용해야 함 |
| dict key에 set 넣고 싶을 때 | frozenset(set_value)로 변환해야 사용 가능 |
s = set()
s.add(set(<[1, 2])) # ❌ TypeError
s.add(frozenset([1, 2])) # ✅ OK
frozenset을 key로 하는 dict를 구현한 뒤 집합 계산HashSet 안에 HashSet을 넣으려면 어떻게 할까?set + defaultdict(set) 콤보로 그래프 구현