해시 문제 분석 2(주의)

류한선·2025년 7월 2일

실기연습-2

목록 보기
73/95

좋아! 이번엔 완전히 다른 스타일로 문제를 낼게.
이번 문제는 실전 실기 문제 스타일 + 코딩 트랩 + 해시 내부 원리가 섞인 응용 문제야.
이번엔 dict.get()기본값 처리, 해시 키 갱신 문제, mutable 객체 값 증가 트랩이 섞여 있어.


🧩 해시 문제 3 — "키가 없을 땐?"

🔸문제:

다음 코드를 실행했을 때 출력 결과로 올바른 것을 고르시오.

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)는 무슨 역할을 하지?
  • 딕셔너리에 key가 없을 때 어떻게 동작하지?
  • counter[item] = counter.get(item, 0) + 1은 몇 번 실행될까?

🧠 이제부터 풀이 해설 시작!


🔹data = ["apple", "banana", "apple", "orange", "banana", "apple"]

  • 과일 이름이 담긴 리스트

  • 총 6개 요소 중:

    • apple: 3회
    • banana: 2회
    • orange: 1회

🔹counter = {}

  • 빈 딕셔너리 생성 → 각 과일 이름의 출현 횟수를 저장할 목적

🔹for 루프 분석

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
    • 없으면 0을 기본값으로 +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)

✅ 정답: A

3
2
1
3

📌 핵심 포인트 요약

  • dict.get(key, default)는 key가 없을 때 default 값을 반환 (하지만 딕셔너리에 추가하지 않음!)
  • counter[item] = counter.get(item, 0) + 1 은 key가 없을 땐 0으로 시작해서 count up
  • len(counter)는 최종적으로 들어간 key 개수

💥 함정 설명: get()만 호출해도 키가 생긴다고 착각하는 경우!

counter.get("apple", 0)  # 단순히 0을 반환할 뿐, 딕셔너리에 key 추가 안 됨!

아래처럼 해야 실제로 key가 생김:

counter["apple"] = counter.get("apple", 0) + 1

좋아! 이번엔 진짜 실무 스타일 해시 문제 + 객체의 __eq__, __hash__ 오버라이딩 실수 트랩이 숨어 있는 유형을 낼게.
사용자 정의 객체를 key로 쓸 때 벌어지는 치명적인 실수를 담고 있어. 정보처리기사 실기나 기업 코딩 테스트에서 실전 트랩으로 자주 출제되는 유형이야.


🧠 해시 문제 4 — "같은 값인데 왜 다른 키지?"

🔸문제:

다음 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일까?
  • 해시 구조에서 key가 같다고 판단되려면 어떤 조건을 충족해야 할까?
  • __hash__가 주석 처리돼 있을까?

🧠 이제부터 전체 풀이!


🔸1. 클래스 정의

class Item:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name
  • 사용자 정의 클래스 Item 선언
  • __eq__() 오버라이딩 → 값으로 비교 가능 (name이 같으면 같은 객체처럼 보이게 됨)
  • BUT ❗ __hash__()는 오버라이딩 안됨
    → 기본적으로 object.__hash__() 사용됨 → 각 인스턴스는 고유한 해시값을 가짐

🔸2. 객체 생성

i1 = Item("pen")
i2 = Item("pen")
  • i1.name == i2.name == "pen"
  • i1 == i2True
  • BUT! hash(i1) != hash(i2) → 서로 다른 해시값을 가짐

💣 해시값이 다르면, dict 입장에서는 서로 다른 키로 간주!


🔸3. 딕셔너리에 추가

d = {}
d[i1] = 100
d[i2] = 200
  • i1i2값이 같아 보여도 다른 해시값을 가지므로,
  • dict는 이 둘을 다른 key로 저장

상태:

d = {
    i1: 100,
    i2: 200
}

따라서 키 개수는 2


🔸4. 출력

print(len(d))  # 2
print(d[i1])   # 100

✅ 정답: C

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)

그렇게 되면 i1i2같은 key로 간주돼, 덮어쓰기됨:

d = {}
d[i1] = 100
d[i2] = 200

print(len(d))   # 1
print(d[i1])    # 200

→ 이 경우 정답은 A가 됨.


좋은 질문이야! 결론부터 말하면:

**"이름(name)을 한 번 더 가져다 쓰는 __hash__() 메서드가 있어야 '값이 같은 객체'를 해시 key로 인정한다"**는 말은 부분적으로 맞지만, 핵심은 다음과 같아.


🔑 핵심 정리

파이썬에서 어떤 객체가 딕셔너리의 키(key) 로 쓰일 수 있으려면 반드시 다음 두 조건을 모두 만족해야 돼:

✅ 조건 1: __hash__() 오버라이딩 → 해시값이 같아야 함

  • 즉, 같은 값(name)을 가진 객체는 같은 hash() 값을 반환해야 함

✅ 조건 2: __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 == i2True
  • 하지만 hash(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 == i2True
  • hash(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()를 호출해야 해?

파이썬에서 어떤 객체를 dictset의 key로 쓰면 다음 두 단계를 수행해:

__hash__()를 호출해서 해시값을 계산함

  • 해시값이 같아야 “혹시 이 키 이미 있는 건가?” 하고 다음 단계로 감

__eq__()로 두 객체를 비교함

  • ==까지 True여야 “이건 같은 키”로 인정하고 덮어쓰기

🎯 왜 return hash(self.name)이어야 하는가?

  • self.namestr 타입 → str은 immutable → 해시 가능
  • hash(self.name) → 항상 같은 값을 반환하므로 안전
  • 따라서 이 객체의 identity(동일성)를 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  # 💥 해시 충돌 유발 가능성 매우 높음
  • 이렇게 하면 모든 인스턴스의 해시값이 똑같아짐
  • dict나 set에서 심각한 성능 저하 발생 → 해시 충돌 너무 많음

❌ 이것도 잘못된 예

def __hash__(self):
    return id(self)  # ❌ 이건 기본 object hash와 같음 (주소 기반)
  • 이건 매번 새 객체마다 다른 해시값 → __eq__()가 True여도 해시가 다르면 딕셔너리에서 다른 키로 취급

🔐 파이썬 공식 규칙 (PEP 8 + Python Data Model 문서)

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.

  • "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에 쓸 수 있는 자료형의 기준을 완전히 이해하고 있는지를 묻는 문제야.


🧠 해시 문제 5 — "집합 안에 집합?"

🔸문제

다음 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은 무엇인가?
  • ab는 값이 같으면 같은 키가 되나?

🔍 정답 풀이


🔹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)

  • frozensetimmutable 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})가 하나만 들어감
  • 출력: 1

✅ 정답: A. 1


🔑 핵심 요약

개념설명
setmutable → hash 불가능 → dict/set의 key로 사용 불가
frozensetimmutable → 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를 구현한 뒤 집합 계산
  • Java에서 HashSet 안에 HashSet을 넣으려면 어떻게 할까?
  • set + defaultdict(set) 콤보로 그래프 구현

0개의 댓글