Magic methods, also called dunder (double underscore) methods, are special methods in Python that allow us to define how objects of a class behave in different situations. They are prefixed and suffixed with double underscores (e.g., __init__
, __str__
, __add__
).
Magic methods enable operator overloading, object customization, and built-in function support.
1. Constructor and Destructor (__init__
, __del__
)
1.1 __init__
– Object Initialization (Constructor)
This method is called when an object is created. It initializes instance attributes.
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def display(self):
print(f"Car: {self.brand} {self.model}")
car1 = Car("Toyota", "Corolla") # Calls __init__()
car1.display()
Why use __init__
?
- Automatically initializes attributes when an object is created.
1.2 __del__
– Object Destruction (Destructor)
Called when an object is about to be deleted or goes out of scope.
class Car:
def __init__(self, brand):
self.brand = brand
print(f"{self.brand} car created.")
def __del__(self):
print(f"{self.brand} car destroyed.")
car1 = Car("Honda")
del car1 # Calls __del__()
Why use __del__
?
- Cleans up resources when an object is deleted.
2. String Representation (__str__
, __repr__
)
2.1 __str__
– User-Friendly String Representation
Used when calling print()
or str()
on an object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person: {self.name}, Age: {self.age}"
p = Person("Alice", 30)
print(p) # Calls __str__()
Why use __str__
?
- Provides a human-readable description of the object.
2.2 __repr__
– Official String Representation
Used for debugging and should return a string that can recreate the object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f'Person("{self.name}", {self.age})'
p = Person("Bob", 25)
print(repr(p)) # Calls __repr__()
Why use __repr__
?
- Used for debugging and should return a string that can recreate the object.
3. Operator Overloading (__add__
, __sub__
, __mul__
, etc.)
3.1 __add__
– Overloading +
Operator
Allows objects to be added using +
.
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 __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Calls __add__()
print(v3) # Output: Vector(6, 8)
Why use __add__
?
- Enables custom addition behavior for objects.
3.2 __sub__
, __mul__
, __truediv__
– Overloading Other Operators
class Number:
def __init__(self, value):
self.value = value
def __sub__(self, other):
return Number(self.value - other.value)
def __mul__(self, other):
return Number(self.value * other.value)
def __truediv__(self, other):
return Number(self.value / other.value)
def __str__(self):
return f"Number({self.value})"
n1 = Number(10)
n2 = Number(5)
print(n1 - n2) # Calls __sub__() → Output: Number(5)
print(n1 * n2) # Calls __mul__() → Output: Number(50)
print(n1 / n2) # Calls __truediv__() → Output: Number(2.0)
Why use these methods?
- Allow mathematical operations on user-defined objects.
4. Comparison Overloading (__eq__
, __lt__
, __gt__
)
Used to compare objects with <
, >
, ==
, etc.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def __eq__(self, other): # Overloads ==
return self.salary == other.salary
def __lt__(self, other): # Overloads <
return self.salary < other.salary
def __gt__(self, other): # Overloads >
return self.salary > other.salary
emp1 = Employee("Alice", 5000)
emp2 = Employee("Bob", 7000)
print(emp1 == emp2) # Calls __eq__() → Output: False
print(emp1 < emp2) # Calls __lt__() → Output: True
print(emp1 > emp2) # Calls __gt__() → Output: False
Why use comparison methods?
- Allow objects to be compared logically.
5. Length and Indexing (__len__
, __getitem__
, __setitem__
)
5.1 __len__
– Returns Length of an Object
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
team = Team(["Alice", "Bob", "Charlie"])
print(len(team)) # Calls __len__() → Output: 3
5.2 __getitem__
, __setitem__
– Indexing and Assignment
class Team:
def __init__(self, members):
self.members = members
def __getitem__(self, index):
return self.members[index]
def __setitem__(self, index, value):
self.members[index] = value
team = Team(["Alice", "Bob", "Charlie"])
print(team[1]) # Calls __getitem__() → Output: Bob
team[1] = "David" # Calls __setitem__()
print(team.members) # Output: ['Alice', 'David', 'Charlie']
Why use these methods?
- Enable custom indexing and slicing for objects.
6. Summary of Magic Methods
Method | Purpose |
---|---|
__init__ | Initializes an object (constructor) |
__del__ | Cleans up resources when an object is deleted |
__str__ | Provides a user-friendly string representation |
__repr__ | Provides an official string representation |
__add__ , __sub__ , __mul__ , etc. | Overloads arithmetic operators |
__eq__ , __lt__ , __gt__ , etc. | Overloads comparison operators |
__len__ | Returns length of an object |
__getitem__ , __setitem__ | Enables indexing and assignment |