해시와 동등성: __hash__와 __eq__의 관계

Python 해시 계약(a==b → hash(a)==hash(b))을 이해하고, __hash__/__eq__ 올바른 구현 방법, dataclass frozen 옵션, 해시 불변성 주의사항을 정리합니다.

· 4 min read · PALDYN Team

지난 글에서 ==is의 차이를 살펴봤다. 이번에는 더 깊이 들어가 **해시(hash)**와 동등성이 어떻게 연결되는지, dictset이 내부적으로 어떻게 동작하는지 이해한다.

해시란

해시는 임의 크기의 데이터를 고정 크기 정수로 변환하는 함수다. Python에서 hash(x)x.__hash__()를 호출한다.

hash("hello")      # 예: -4177612485710190685 (실행마다 다를 수 있음)
hash(42)           # 42
hash((1, 2, 3))    # 2528502973977326415
hash([1, 2, 3])    # TypeError: unhashable type: 'list'

해시 계약 (Hash Contract)

Python에는 반드시 지켜야 할 규칙이 있다.

a == b 이면 반드시 hash(a) == hash(b) 여야 한다.

역은 성립하지 않는다. hash(a) == hash(b) 여도 a != b일 수 있다(해시 충돌). dictset은 이 계약을 전제로 동작한다.

# 계약 확인
hash(1) == hash(1.0)    # True (1 == 1.0 이므로)
hash(True) == hash(1)   # True (True == 1 이므로)

해시와 동등성 개요

__eq__만 재정의하면?

Python은 __eq__를 재정의하면 __hash__None으로 설정한다. 해시 계약을 자동으로 깨지 않기 위해서다.

class Bad:
    def __eq__(self, other):
        return True

hash(Bad())   # TypeError: unhashable type: 'Bad'

dict 키나 set 원소로 쓰려면 __hash__도 함께 구현해야 한다.

올바른 hash / eq 구현

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))  # __eq__에 쓰인 필드로만

p1 = Point(1, 2)
p2 = Point(1, 2)

p1 == p2          # True
{p1, p2}          # {Point(1,2)} — 집합 크기 1
d = {p1: "val"}
d[p2]             # "val" — p1 == p2 이고 hash 같으므로

해시 불변성 — 절대 지켜야 할 규칙

객체가 dict 키나 set 원소로 들어간 후 해시 값이 바뀌면 찾을 수 없다.

class MutablePoint:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __hash__(self):
        return hash((self.x, self.y))

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

p = MutablePoint(1, 2)
d = {p: "ok"}

p.x = 99        # 해시값이 바뀜!
d[p]            # KeyError — 버킷이 달라져 못 찾음

해시 가능 객체는 라이프사이클 동안 해시값이 불변이어야 한다. 이 때문에 가변 객체는 기본적으로 해시 불가다.

dataclass와 frozen

dataclass를 사용하면 __hash__를 쉽게 관리할 수 있다.

from dataclasses import dataclass

# eq=True (기본): __eq__ 생성, __hash__=None
@dataclass
class Mutable:
    x: int

# frozen=True: 불변 + __hash__ 자동 생성
@dataclass(frozen=True)
class Immutable:
    x: int
    y: int

p = Immutable(1, 2)
hash(p)       # OK
p.x = 10     # FrozenInstanceError
d = {p: "key"}  # dict 키 사용 가능

dataclass frozen

내장 타입 해시 가능 여부

타입해시 가능이유
int, float, bool불변
str, bytes불변
tuple조건부원소가 모두 해시 가능하면
list, dict, set가변
frozenset불변 집합
사용자 클래스기본 ✓id 기반 __hash__ 상속

지난 글: 동등성 vs 동일성: == 와 is 의 차이


읽어주셔서 감사합니다. 😊