__eq__와 __hash__: 동등성과 해시 계약

Python __eq__와 __hash__의 관계, 동등성 계약, dict 키와 set 원소로 사용하기 위한 올바른 구현 방법을 설명합니다.

· 4 min read · PALDYN Team

지난 글에서 __repr____str__을 구현하는 법을 살펴보았습니다. 이번에는 객체의 동등성(equality) 을 정의하는 __eq__해시(hash) 를 정의하는 __hash__를 다룹니다. 이 두 메서드는 서로 강하게 연결된 계약을 형성합니다.

동일성 vs 동등성

Python에는 두 가지 비교 개념이 있습니다.

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a is c)    # True  — 동일성: 같은 객체 (identity)
print(a is b)    # False — 다른 객체
print(a == b)    # True  — 동등성: 값이 같음 (equality)

is는 메모리 주소(id)를 비교합니다. ==__eq__ 메서드를 호출합니다. 기본적으로 사용자 정의 클래스는 __eq__를 구현하지 않으면 is와 동일하게 동작합니다.

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)

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)   # True
print(p1 == p3)   # False
print(p1 == "xy") # False (NotImplemented → Python이 역방향 시도 후 False)

isinstance 확인이 필요한 이유는 다른 타입과의 비교 시 NotImplemented를 반환해야 Python이 역방향 비교(other.__eq__(self))를 시도할 수 있기 때문입니다. False를 바로 반환하면 이 기회를 차단합니다.

__eq__와 hash 계약

a == b  →  반드시  hash(a) == hash(b)

Python은 __eq__를 정의하면 자동으로 __hash__None으로 설정합니다. 이는 위 계약을 보장하기 위해서입니다. __eq__를 정의한 클래스를 dict 키나 set 원소로 쓰려면 __hash__도 함께 구현해야 합니다.

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))  # 동등한 객체는 같은 해시

hash(tuple)을 활용하면 쉽게 올바른 해시를 만들 수 있습니다. __eq__에서 비교하는 필드를 그대로 튜플로 묶어 해시하면 계약이 자동으로 성립합니다.

__eq__와 hash 계약

Point 클래스 구현

@dataclass가 처리하는 방식

@dataclass는 기본적으로 __eq__를 생성하지만 __hash__는 생성하지 않습니다. frozen=True로 불변 dataclass를 만들면 __hash__도 함께 생성됩니다.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float
    # __eq__와 __hash__ 자동 생성

p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1 == p2)           # True
print({p1, p2})           # {Point(x=1.0, y=2.0)} — 중복 제거
print({p1: "origin"})     # {Point(x=1.0, y=2.0): 'origin'}

가변 dataclass에서 해시가 필요하면 unsafe_hash=True를 사용할 수 있지만, 가변 객체를 dict 키로 사용하는 것은 위험합니다.

hash(None)과 불변성

해시 가능한 객체는 일반적으로 불변(immutable) 이어야 합니다. 가변 객체가 해시 가능하면 상태 변경 후 해시값이 달라져 dict에서 찾을 수 없게 됩니다.

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

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

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

p = BadPoint(1, 2)
d = {p: "value"}
p.x = 99   # 상태 변경!
print(d[p])  # KeyError — 해시가 달라졌기 때문

__ne__ 자동 정의

Python 3에서 __eq__를 정의하면 __ne__는 자동으로 not __eq__(...)로 구현됩니다. 직접 정의할 필요가 없습니다.


지난 글: repr vs str: 객체를 문자열로

다음 글: 비교 메서드: lt, le, gt, ge


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