# 24. Operator Overloading

***

**1. Overloading `+` for Adding Two Objects**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(4, 6)
```

***

**2. Overloading `-` for Subtraction**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(5, 7)
v2 = Vector(2, 3)
print(v1 - v2)  # Vector(3, 4)
```

***

**3. Overloading `*` for Scalar Multiplication**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(3, 4)
print(v * 2)  # Vector(6, 8)
```

***

**4. Overloading `/` for Scalar Division**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __truediv__(self, scalar):
        return Vector(self.x / scalar, self.y / scalar)

    def __repr__(self):
        return f"Vector({self.x:.2f}, {self.y:.2f})"

v = Vector(8, 6)
print(v / 2)  # Vector(4.00, 3.00)
```

***

**5. Overloading `==` for Equality Comparison**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

v1 = Vector(2, 3)
v2 = Vector(2, 3)
v3 = Vector(4, 5)
print(v1 == v2)  # True
print(v1 == v3)  # False
```

***

**6. Overloading `>` for Greater Than Comparison**

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __gt__(self, other):
        return (self.x ** 2 + self.y ** 2) > (other.x ** 2 + other.y ** 2)

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 > v2)  # True
```

***

**7. Overloading `%` for Custom Modulo Behavior**

```python
class CustomNumber:
    def __init__(self, value):
        self.value = value

    def __mod__(self, other):
        return self.value % other.value + 1

n1 = CustomNumber(10)
n2 = CustomNumber(3)
print(n1 % n2)  # 2 (10 % 3 + 1)
```

***

**8. Overloading `len()` Using `__len__`**

```python
class CustomList:
    def __init__(self, elements):
        self.elements = elements

    def __len__(self):
        return len(self.elements)

lst = CustomList([1, 2, 3, 4])
print(len(lst))  # 4
```

***

**9. Overloading String Representation Using `__str__`**

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

    def __str__(self):
        return f"{self.name}, Age: {self.age}"

p = Person("Alice", 30)
print(p)  # Alice, Age: 30
```

***

**10. Overloading `in` Using `__contains__`**

```python
class CustomContainer:
    def __init__(self, elements):
        self.elements = elements

    def __contains__(self, item):
        return item in self.elements

container = CustomContainer([1, 2, 3, 4])
print(3 in container)  # True
print(5 in container)  # False
```

***

These examples illustrate how Python's special methods (dunder methods) allow you to redefine the behavior of built-in operators to work with custom classes, providing an intuitive and readable API.
