← Назад к вопросам

Какие знаешь магические (dunder) методы для работы с хэшируемыми объектами в Python?

1.7 Middle🔥 81 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Какие знаешь магические (dunder) методы для работы с хэшируемыми объектами в Python

Магические методы (dunder methods, double underscore) — это специальные методы Python, которые начинаются и заканчиваются двойным подчеркиванием. Для хешируемых объектов наиболее важны методы __hash__() и __eq__().

Основные методы для хешируемости

1. __hash__(self) — вычисление хеша объекта

Возвращает целое число, используется для размещения объекта в хеш-таблице.

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def __hash__(self):
        """Вычислить хеш на основе email"""
        return hash(self.email)
    
    def __eq__(self, other):
        """Два пользователя равны, если у них одинаковый email"""
        if not isinstance(other, User):
            return False
        return self.email == other.email

# Использование
user1 = User("Alice", "alice@example.com")
user2 = User("Alice", "alice@example.com")
user3 = User("Bob", "bob@example.com")

print(hash(user1))  # Одинаковое значение
print(hash(user2))  # для user1 и user2

print(user1 == user2)  # True
print(user1 == user3)  # False

# Можно использовать в множеством
users = {user1, user2, user3}  # user1 и user2 - это один элемент
print(len(users))  # 2

2. __eq__(self, other) — проверка равенства

Определяет, когда два объекта считаются равными.

class Product:
    def __init__(self, id, name, price):
        self.id = id
        self.name = name
        self.price = price
    
    def __hash__(self):
        """Хеш по ID"""
        return hash(self.id)
    
    def __eq__(self, other):
        """Равны по ID"""
        if not isinstance(other, Product):
            return NotImplemented
        return self.id == other.id

# Правило: если a == b, то hash(a) == hash(b)
p1 = Product(1, "Laptop", 1000)
p2 = Product(1, "Laptop", 1000)  # Тот же ID
p3 = Product(2, "Phone", 500)    # Другой ID

print(p1 == p2)  # True
print(hash(p1) == hash(p2))  # True - это ВАЖНО!

# В словаре и множестве
products = {p1, p2, p3}
print(len(products))  # 2, так как p1 == p2

product_dict = {p1: "In stock"}
product_dict[p2] = "Updated"  # Обновляет существующий ключ
print(len(product_dict))  # 1

Дополнительные dunder методы

3. __repr__(self) — строковое представление

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __hash__(self):
        return hash((self.x, self.y))
    
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    
    def __repr__(self):
        """Красивый вывод в интерпретаторе"""
        return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(repr(p))  # Point(3, 4)
print(str(p))   # Point(3, 4)

4. __lt__, __le__, __gt__, __ge__ — сравнения

class Version:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch
    
    def __hash__(self):
        return hash((self.major, self.minor, self.patch))
    
    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor, self.patch) == \
               (other.major, other.minor, other.patch)
    
    def __lt__(self, other):
        """Меньше"""
        return (self.major, self.minor, self.patch) < \
               (other.major, other.minor, other.patch)
    
    def __repr__(self):
        return f"{self.major}.{self.minor}.{self.patch}"

v1 = Version(1, 2, 3)
v2 = Version(1, 2, 4)
v3 = Version(1, 2, 3)

print(v1 == v3)  # True
print(v1 < v2)   # True
print(hash(v1) == hash(v3))  # True

Правила для хешируемости

# ✅ ПРАВИЛО 1: Если a == b, то hash(a) == hash(b)
class Good:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return hash(self.value)
    
    def __eq__(self, other):
        if not isinstance(other, Good):
            return NotImplemented
        return self.value == other.value

# ❌ ПРАВИЛО 2: Нарушение (плохо!)
class Bad:
    def __init__(self, value, mutable_list):
        self.value = value
        self.mutable_list = mutable_list
    
    def __hash__(self):
        # ПРОБЛЕМА: хеш зависит от неизменяемого значения
        return hash(self.value)
    
    def __eq__(self, other):
        # А равенство проверяет изменяемый список
        if not isinstance(other, Bad):
            return NotImplemented
        return self.value == other.value and \
               self.mutable_list == other.mutable_list

b1 = Bad(1, [1, 2, 3])
b2 = Bad(1, [1, 2, 3])

print(b1 == b2)        # True
print(hash(b1) == hash(b2))  # True (совпадает)

b2.mutable_list.append(4)
print(b1 == b2)        # False! А хеш остался прежним - ОШИБКА!

Практические примеры

Использование в кэшировании (LRU Cache)

from functools import lru_cache

class CacheableObject:
    def __init__(self, data):
        self.data = data
    
    def __hash__(self):
        return hash(self.data)
    
    def __eq__(self, other):
        if not isinstance(other, CacheableObject):
            return NotImplemented
        return self.data == other.data
    
    def __repr__(self):
        return f"CacheableObject({self.data})"

@lru_cache(maxsize=128)
def expensive_operation(obj):
    """Результаты кэшируются на основе хеша объекта"""
    return obj.data ** 2

obj1 = CacheableObject(5)
obj2 = CacheableObject(5)

result1 = expensive_operation(obj1)  # Вычисляется
result2 = expensive_operation(obj2)  # Берется из кэша

Использование в словарях

class Employee:
    def __init__(self, employee_id, name):
        self.employee_id = employee_id
        self.name = name
    
    def __hash__(self):
        return hash(self.employee_id)
    
    def __eq__(self, other):
        if not isinstance(other, Employee):
            return NotImplemented
        return self.employee_id == other.employee_id
    
    def __repr__(self):
        return f"Employee({self.employee_id}, {self.name})"

emp1 = Employee(1, "Alice")
emp2 = Employee(1, "Alice Updated")
emp3 = Employee(2, "Bob")

employee_data = {emp1: "Senior Developer"}
employee_data[emp2] = "Lead Developer"  # Обновляет emp1
employee_data[emp3] = "Manager"

print(len(employee_data))  # 2
print(employee_data)

Важные моменты

  1. Только неизменяемые объекты должны быть хешируемы — избегай зависимостей от изменяемых атрибутов
  2. hash() должен быть идентичен для равных объектов — это критично
  3. Используй tuple для композитных хешейhash((a, b, c))
  4. Вернуть NotImplemented в eq — если тип не совпадает
  5. Документируй поведение — объясни, на основе каких полей вычисляется хеш
Какие знаешь магические (dunder) методы для работы с хэшируемыми объектами в Python? | PrepBro