# 34. Custom Hashable Classes

Here are 10 Python code snippets demonstrating how to implement `__hash__` and `__eq__` for custom objects, making them hashable and usable in hashable collections like sets and dictionaries.

***

**1. Basic Hashable Class**

This example shows how to create a simple custom class that is hashable.

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return (self.name, self.age) == (other.name, other.age)

    def __hash__(self):
        return hash((self.name, self.age))

# Create instances
p1 = Person("Alice", 30)
p2 = Person("Alice", 30)

# Use in a set
people = {p1, p2}
print(len(people))  # Output: 1, since p1 and p2 are considered equal
```

***

**2. Hashable with Mutable Fields**

This class demonstrates a hashable object with mutable fields but ensures the hash remains valid.

```python
class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model

    def __eq__(self, other):
        return (self.make, self.model) == (other.make, other.model)

    def __hash__(self):
        return hash((self.make, self.model))

# Create instances
car1 = Car("Toyota", "Corolla")
car2 = Car("Toyota", "Corolla")

# Use in a set
cars = {car1, car2}
print(len(cars))  # Output: 1
```

***

**3. Using `frozenset` for Nested Objects**

Here’s a hashable class with a `frozenset` that makes it immutable.

```python
class Country:
    def __init__(self, name, cities):
        self.name = name
        self.cities = frozenset(cities)

    def __eq__(self, other):
        return (self.name, self.cities) == (other.name, other.cities)

    def __hash__(self):
        return hash((self.name, self.cities))

# Create instances
country1 = Country("India", ["Delhi", "Mumbai"])
country2 = Country("India", ["Delhi", "Mumbai"])

# Use in a set
countries = {country1, country2}
print(len(countries))  # Output: 1
```

***

**4. Hashable with Tuple Field**

Using a tuple as an argument to make an object hashable.

```python
class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def __eq__(self, other):
        return (self.id, self.name) == (other.id, other.name)

    def __hash__(self):
        return hash((self.id, self.name))

# Create instances
emp1 = Employee(101, "John")
emp2 = Employee(101, "John")

# Use in a set
employees = {emp1, emp2}
print(len(employees))  # Output: 1
```

***

**5. Hashable with `__slots__`**

An optimized version of a class using `__slots__` for better memory usage.

```python
class Product:
    __slots__ = ('name', 'price')

    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __eq__(self, other):
        return (self.name, self.price) == (other.name, other.price)

    def __hash__(self):
        return hash((self.name, self.price))

# Create instances
product1 = Product("Laptop", 1000)
product2 = Product("Laptop", 1000)

# Use in a set
products = {product1, product2}
print(len(products))  # Output: 1
```

***

**6. Hashable with Multiple Attributes**

Creating a hashable object with more than two attributes.

```python
class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

    def __eq__(self, other):
        return (self.title, self.author, self.year) == (other.title, other.author, other.year)

    def __hash__(self):
        return hash((self.title, self.author, self.year))

# Create instances
book1 = Book("Python Basics", "John Doe", 2021)
book2 = Book("Python Basics", "John Doe", 2021)

# Use in a set
books = {book1, book2}
print(len(books))  # Output: 1
```

***

**7. Custom Hashing Algorithm**

You can use a custom hashing algorithm.

```python
class Student:
    def __init__(self, name, roll_no):
        self.name = name
        self.roll_no = roll_no

    def __eq__(self, other):
        return (self.name, self.roll_no) == (other.name, other.roll_no)

    def __hash__(self):
        # Custom hash combining the string length and roll number
        return hash((len(self.name), self.roll_no))

# Create instances
student1 = Student("Alice", 101)
student2 = Student("Alice", 101)

# Use in a set
students = {student1, student2}
print(len(students))  # Output: 1
```

***

**8. Handling Mutable and Immutable Types Together**

This example involves a class where one of the attributes is mutable.

```python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.dimensions = (width, height)  # Immutable

    def __eq__(self, other):
        return self.dimensions == other.dimensions

    def __hash__(self):
        return hash(self.dimensions)

# Create instances
rect1 = Rectangle(10, 20)
rect2 = Rectangle(10, 20)

# Use in a set
rectangles = {rect1, rect2}
print(len(rectangles))  # Output: 1
```

***

**9. Immutable Class with `NamedTuple`**

Using `namedtuple` to create a hashable class with named fields.

```python
from collections import namedtuple

Person = namedtuple("Person", ["name", "age"])

# Instances are automatically hashable
person1 = Person("Bob", 25)
person2 = Person("Bob", 25)

# Use in a set
people = {person1, person2}
print(len(people))  # Output: 1
```

***

**10. Custom Hash with Cache**

In this example, a cache is used to optimize repeated hash computations.

```python
class Circle:
    def __init__(self, radius):
        self.radius = radius
        self._hash = None  # Cache for hash value

    def __eq__(self, other):
        return self.radius == other.radius

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(self.radius)  # Compute hash once and cache it
        return self._hash

# Create instances
circle1 = Circle(5)
circle2 = Circle(5)

# Use in a set
circles = {circle1, circle2}
print(len(circles))  # Output: 1
```

***

These snippets show various ways to implement `__hash__` and `__eq__` for custom objects, ensuring they can be used in hashable collections like sets and dictionaries. Each example tackles different use cases, such as immutable fields, mutable fields, and caching for optimization.
