Special Methods: Magical Python Classes
Welcome to Special Methods! These are Python’s “magic methods” that start and end with double underscores. They make your classes work seamlessly with Python’s built-in operations and syntax.
What Are Special Methods?
Special methods (also called dunder methods) allow your objects to integrate with Python’s syntax and built-in functions. They’re the “magic” behind operator overloading and object behavior.
Basic Object Creation
class Person:
def __init__(self, name, age):
"""Called when creating a new instance."""
self.name = name
self.age = age
print(f"Person {name} created!")
def __del__(self):
"""Called when object is about to be destroyed."""
print(f"Person {self.name} is being destroyed!")
def __str__(self):
"""Called by str() and print()."""
return f"Person(name='{self.name}', age={self.age})"
def __repr__(self):
"""Called by repr() - should be unambiguous."""
return f"Person('{self.name}', {self.age})"
# Test
person = Person("Alice", 30) # __init__ called
print(person) # __str__ called
print(repr(person)) # __repr__ called
del person # __del__ called
Comparison Operators
Rich Comparison Methods
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other): # ==
if isinstance(other, Student):
return self.grade == other.grade
return NotImplemented
def __lt__(self, other): # <
if isinstance(other, Student):
return self.grade < other.grade
return NotImplemented
def __le__(self, other): # <=
return self == other or self < other
def __gt__(self, other): # >
return not (self <= other)
def __ge__(self, other): # >=
return not (self < other)
def __str__(self):
return f"{self.name}: {self.grade}"
# Test comparisons
students = [
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78)
]
print("Students:", [str(s) for s in students])
print("Sorted by grade:", [str(s) for s in sorted(students)])
alice = students[0]
bob = students[1]
print(f"Alice == Bob: {alice == bob}")
print(f"Alice < Bob: {alice < bob}")
Arithmetic Operators
Mathematical Operations
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # self + other
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other): # self - other
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar): # self * scalar
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar): # scalar * self (reflected)
return self * scalar # Reuse __mul__
def __truediv__(self, scalar): # self / scalar
if isinstance(scalar, (int, float)):
return Vector(self.x / scalar, self.y / scalar)
return NotImplemented
def __neg__(self): # -self
return Vector(-self.x, -self.y)
def __abs__(self): # abs(self)
return (self.x ** 2 + self.y ** 2) ** 0.5
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Test arithmetic operations
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v1 + v2: {v1 + v2}")
print(f"v1 - v2: {v1 - v2}")
print(f"v1 * 3: {v1 * 3}")
print(f"2 * v1: {2 * v1}") # Uses __rmul__
print(f"v1 / 2: {v1 / 2}")
print(f"-v1: {-v1}")
print(f"abs(v1): {abs(v1):.2f}")
Container Methods
Making Objects Work Like Lists/Dictionaries
class SimpleList:
def __init__(self):
self._items = []
def __len__(self): # len(obj)
return len(self._items)
def __getitem__(self, index): # obj[index]
return self._items[index]
def __setitem__(self, index, value): # obj[index] = value
self._items[index] = value
def __delitem__(self, index): # del obj[index]
del self._items[index]
def __iter__(self): # for item in obj:
return iter(self._items)
def __contains__(self, item): # item in obj
return item in self._items
def append(self, item):
self._items.append(item)
# Test container methods
my_list = SimpleList()
my_list.append("apple")
my_list.append("banana")
my_list.append("cherry")
print(f"Length: {len(my_list)}")
print(f"First item: {my_list[0]}")
print(f"Contains 'banana': {'banana' in my_list}")
print(f"All items: {list(my_list)}")
my_list[1] = "orange"
print(f"After change: {list(my_list)}")
del my_list[0]
print(f"After deletion: {list(my_list)}")
Dictionary-like Behavior
class CaseInsensitiveDict:
def __init__(self, data=None):
self._data = {}
if data:
for key, value in data.items():
self[key] = value
def __setitem__(self, key, value): # obj[key] = value
self._data[key.lower()] = (key, value)
def __getitem__(self, key): # obj[key]
return self._data[key.lower()][1]
def __delitem__(self, key): # del obj[key]
del self._data[key.lower()]
def __iter__(self): # for key in obj:
return (original_key for original_key, _ in self._data.values())
def __len__(self): # len(obj)
return len(self._data)
def __contains__(self, key): # key in obj
return key.lower() in self._data
def keys(self):
return [original_key for original_key, _ in self._data.values()]
def values(self):
return [value for _, value in self._data.values()]
def items(self):
return self._data.values()
# Test dictionary-like behavior
d = CaseInsensitiveDict()
d["Name"] = "Alice"
d["AGE"] = 30
d["City"] = "New York"
print(f"Keys: {list(d.keys())}")
print(f"Values: {list(d.values())}")
print(f"Length: {len(d)}")
print(f"'name' in dict: {'name' in d}")
print(f"'Name' in dict: {'Name' in d}")
print(f"d['name']: {d['name']}")
print(f"d['AGE']: {d['AGE']}")
Context Managers
__enter__ and __exit__ Methods
class Timer:
def __init__(self, name="Timer"):
self.name = name
def __enter__(self):
import time
self.start_time = time.time()
print(f"Starting {self.name}...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.time() - self.start_time
print(f"{self.name} completed in {elapsed:.2f} seconds")
# Return False to propagate exceptions
return False
# Test context manager
with Timer("Data Processing") as t:
# Simulate some work
import time
time.sleep(1)
print("Processing data...")
print("Done!")
File-like Object
class StringIO:
"""Simple string-based file-like object."""
def __init__(self):
self._content = ""
self._position = 0
def write(self, text):
self._content += text
def read(self, size=-1):
if size == -1:
result = self._content[self._position:]
self._position = len(self._content)
else:
result = self._content[self._position:self._position + size]
self._position += len(result)
return result
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Could flush or close here
pass
# Test file-like object
with StringIO() as f:
f.write("Hello, ")
f.write("World!")
f.write(" How are you?")
content = f.read()
print(f"Content: {content}")
f.write(" I'm fine!")
more_content = f.read()
print(f"More content: {more_content}")
Callable Objects
__call__ Method
class Polynomial:
def __init__(self, coefficients):
"""Coefficients from lowest to highest degree."""
self.coefficients = coefficients
def __call__(self, x):
"""Evaluate polynomial at x."""
return sum(coeff * (x ** i) for i, coeff in enumerate(self.coefficients))
def __str__(self):
terms = []
for i, coeff in enumerate(self.coefficients):
if coeff == 0:
continue
if i == 0:
terms.append(str(coeff))
elif i == 1:
terms.append(f"{coeff}x")
else:
terms.append(f"{coeff}x^{i}")
return " + ".join(reversed(terms)) if terms else "0"
# Test callable object
p = Polynomial([1, 2, 1]) # x^2 + 2x + 1
print(f"Polynomial: {p}")
print(f"p(0) = {p(0)}")
print(f"p(1) = {p(1)}")
print(f"p(2) = {p(2)}")
# Can be used like a function
values = [p(x) for x in range(-2, 3)]
print(f"Values from x=-2 to x=2: {values}")
Attribute Access
Customizing Attribute Access
class ValidatedAttributes:
def __init__(self):
self._data = {}
def __setattr__(self, name, value):
if name.startswith('_'):
# Allow private attributes
super().__setattr__(name, value)
else:
# Validate public attributes
if isinstance(value, str) and len(value.strip()) == 0:
raise ValueError(f"Attribute '{name}' cannot be empty string")
self._data[name] = value
def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def __delattr__(self, name):
if name in self._data:
del self._data[name]
else:
super().__delattr__(name)
# Test attribute access control
obj = ValidatedAttributes()
obj.name = "Alice"
obj.age = 30
print(f"Name: {obj.name}")
print(f"Age: {obj.age}")
try:
obj.empty = ""
except ValueError as e:
print(f"Error: {e}")
try:
print(obj.nonexistent)
except AttributeError as e:
print(f"Error: {e}")
Practical Examples
Example 1: Enhanced Fraction Class
import math
class Fraction:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
# Simplify fraction
gcd = math.gcd(numerator, denominator)
self.numerator = numerator // gcd
self.denominator = denominator // gcd
# Make sure denominator is positive
if self.denominator < 0:
self.numerator = -self.numerator
self.denominator = -self.denominator
def __add__(self, other):
if isinstance(other, Fraction):
new_num = self.numerator * other.denominator + other.numerator * self.denominator
new_den = self.denominator * other.denominator
return Fraction(new_num, new_den)
elif isinstance(other, (int, float)):
return self + Fraction(other)
return NotImplemented
def __sub__(self, other):
return self + (-other)
def __mul__(self, other):
if isinstance(other, Fraction):
return Fraction(self.numerator * other.numerator, self.denominator * other.denominator)
elif isinstance(other, (int, float)):
return Fraction(self.numerator * other, self.denominator)
return NotImplemented
def __truediv__(self, other):
if isinstance(other, Fraction):
return self * Fraction(other.denominator, other.numerator)
elif isinstance(other, (int, float)):
return self * Fraction(1, other)
return NotImplemented
def __neg__(self):
return Fraction(-self.numerator, self.denominator)
def __eq__(self, other):
if isinstance(other, Fraction):
return self.numerator * other.denominator == other.numerator * self.denominator
elif isinstance(other, (int, float)):
return self.numerator / self.denominator == other
return False
def __lt__(self, other):
if isinstance(other, Fraction):
return self.numerator * other.denominator < other.numerator * self.denominator
elif isinstance(other, (int, float)):
return self.numerator / self.denominator < other
return NotImplemented
def __le__(self, other):
return self < other or self == other
def __gt__(self, other):
return not (self <= other)
def __ge__(self, other):
return not (self < other)
def __float__(self):
return self.numerator / self.denominator
def __str__(self):
if self.denominator == 1:
return str(self.numerator)
return f"{self.numerator}/{self.denominator}"
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
# Test fraction operations
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)
print(f"f1: {f1}")
print(f"f2: {f2}")
print(f"f1 + f2: {f1 + f2}")
print(f"f1 - f2: {f1 - f2}")
print(f"f1 * f2: {f1 * f2}")
print(f"f1 / f2: {f1 / f2}")
print(f"f1 == 0.5: {f1 == 0.5}")
print(f"f1 < f2: {f1 < f2}")
print(f"float(f1): {float(f1)}")
Example 2: Matrix Class
class Matrix:
def __init__(self, data):
"""data should be a list of lists (rows)."""
if not data or not all(isinstance(row, list) for row in data):
raise ValueError("Matrix data must be a list of lists")
self.rows = len(data)
self.cols = len(data[0])
if not all(len(row) == self.cols for row in data):
raise ValueError("All rows must have the same length")
self.data = [row[:] for row in data] # Deep copy
def __getitem__(self, key):
"""matrix[i, j] or matrix[i][j]"""
if isinstance(key, tuple) and len(key) == 2:
i, j = key
return self.data[i][j]
else:
return self.data[key]
def __setitem__(self, key, value):
"""matrix[i, j] = value"""
if isinstance(key, tuple) and len(key) == 2:
i, j = key
self.data[i][j] = value
else:
self.data[key] = value
def __add__(self, other):
if not isinstance(other, Matrix):
return NotImplemented
if self.rows != other.rows or self.cols != other.cols:
raise ValueError("Matrices must have the same dimensions")
result = []
for i in range(self.rows):
row = []
for j in range(self.cols):
row.append(self.data[i][j] + other.data[i][j])
result.append(row)
return Matrix(result)
def __mul__(self, other):
if isinstance(other, Matrix):
# Matrix multiplication
if self.cols != other.rows:
raise ValueError("Matrix dimensions don't match for multiplication")
result = []
for i in range(self.rows):
row = []
for j in range(other.cols):
sum_val = 0
for k in range(self.cols):
sum_val += self.data[i][k] * other.data[k][j]
row.append(sum_val)
result.append(row)
return Matrix(result)
elif isinstance(other, (int, float)):
# Scalar multiplication
result = []
for i in range(self.rows):
row = []
for j in range(self.cols):
row.append(self.data[i][j] * other)
result.append(row)
return Matrix(result)
return NotImplemented
def __eq__(self, other):
if not isinstance(other, Matrix):
return False
return self.data == other.data
def __str__(self):
return "\n".join([" ".join(map(str, row)) for row in self.data])
def __repr__(self):
return f"Matrix({self.data})"
# Test matrix operations
m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[5, 6], [7, 8]])
print("Matrix 1:")
print(m1)
print("\nMatrix 2:")
print(m2)
print("\nMatrix 1 + Matrix 2:")
print(m1 + m2)
print("\nMatrix 1 * Matrix 2:")
print(m1 * m2)
print("\nMatrix 1 * 2:")
print(m1 * 2)
print(f"\nElement [0,1]: {m1[0,1]}")
m1[0, 1] = 99
print(f"After change: {m1[0,1]}")
Example 3: Custom Collection
class PriorityQueue:
def __init__(self):
self._items = []
def push(self, item, priority=0):
"""Add item with priority."""
self._items.append((priority, item))
self._items.sort(reverse=True) # Highest priority first
def pop(self):
"""Remove and return highest priority item."""
if not self._items:
raise IndexError("Queue is empty")
return self._items.pop(0)[1]
def peek(self):
"""Return highest priority item without removing."""
if not self._items:
raise IndexError("Queue is empty")
return self._items[0][1]
def __len__(self):
return len(self._items)
def __bool__(self):
return bool(self._items)
def __iter__(self):
return (item for _, item in self._items)
def __str__(self):
items_str = ", ".join(str(item) for item in self)
return f"PriorityQueue([{items_str}])"
def __eq__(self, other):
if not isinstance(other, PriorityQueue):
return False
return self._items == other._items
# Test priority queue
pq = PriorityQueue()
pq.push("Low priority task", priority=1)
pq.push("High priority task", priority=5)
pq.push("Medium priority task", priority=3)
print(f"Queue: {pq}")
print(f"Length: {len(pq)}")
print(f"Is empty: {not pq}")
print(f"Next task: {pq.peek()}")
completed_task = pq.pop()
print(f"Completed: {completed_task}")
print(f"Remaining: {pq}")
Practice Exercises
Exercise 1: Complex Number Class
Create a ComplexNumber class with:
__init__,__str__,__repr__- Arithmetic operators (
+,-,*,/) - Comparison operators (
==,!=) abs()support with__abs__- Conjugate method
Exercise 2: Time Duration Class
Build a Duration class that:
- Stores time in seconds internally
- Supports creation from hours/minutes/seconds
- Implements arithmetic operations
- Provides string formatting
- Supports comparison operations
Exercise 3: Sparse Array
Create a SparseArray class that:
- Efficiently stores mostly-zero arrays
- Implements
__getitem__,__setitem__ - Supports
len()and iteration - Provides memory usage information
- Handles negative indices
Exercise 4: Configuration Object
Build a Config class with:
- Dictionary-like access (
config['key']) - Attribute access (
config.key) - Validation of values
- Loading from/saving to JSON
- Environment variable override
Exercise 5: Database Connection Pool
Create a ConnectionPool class that:
- Manages multiple database connections
- Implements context manager protocol
- Provides connection checkout/checkin
- Tracks connection usage statistics
- Handles connection failures gracefully
Summary
Special methods make your classes integrate seamlessly with Python:
Object Creation/Destruction:
def __init__(self, ...): pass # Constructor
def __del__(self): pass # Destructor
def __str__(self): pass # String representation
def __repr__(self): pass # Unambiguous representation
Operators:
def __add__(self, other): pass # +
def __eq__(self, other): pass # ==
def __lt__(self, other): pass # <
def __len__(self): pass # len()
def __getitem__(self, key): pass # obj[key]
Context Managers:
def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb): pass
Callable Objects:
def __call__(self, ...): pass # obj(args)
Key Benefits:
- Natural Python syntax integration
- Operator overloading
- Custom object behavior
- Container protocol implementation
- Context manager support
Common Special Methods:
__init__,__str__,__repr__- Basic object setup__eq__,__lt__, etc. - Comparisons__add__,__mul__, etc. - Arithmetic__len__,__getitem__- Container behavior__enter__,__exit__- Context managers
Next: Advanced OOP - taking your skills to the next level! 🚀