Leveraging Dunder Methods to Customize Python Class Behavior
Python enables developers to integrate custom objects into standard language syntax by implementing specific double-underscore methods. These special methods intercept built-in function calls and operator overloads, allowing instances to mimic native types like lists, dictionaries, or strings.
Intercepting Length Queries
The len() function does not directly measure object size. Instead, it delegates to the object's __len__ protoocl. Implementing this method allows a class to define its own size calculation logic, typically returning an integer representing the number of contained elements.
class QueueBuffer:
def __init__(self, capacity_limit):
self.capacity = capacity_limit
self.active_tasks = []
def add_task(self, task):
if len(self.active_tasks) < self.capacity:
self.active_tasks.append(task)
def __len__(self):
return len(self.active_tasks)
job_queue = QueueBuffer(100)
job_queue.add_task("process_data")
print(len(job_queue)) # Outputs: 1
Managing Index-Based Operations
Accessing, modifying, or removing elements via square brackets triggers distinct dunder methods. The [] operator for reading maps to __getitem__, assignment maps to __setitem__, and the del keyword invokes __delitem__. Proper implementation requires handling both valid indices and boundary conditions.
class IndexedRegistry:
def __init__(self, initial_values=None):
self._storage = list(initial_values) if initial_values else []
def __getitem__(self, position):
return self._storage[position]
def __setitem__(self, position, new_value):
self._storage[position] = new_value
def __delitem__(self, position):
del self._storage[position]
registry = IndexedRegistry(["alpha", "beta", "gamma"])
registry[1] = "delta"
del registry[0]
print(registry[0]) # Outputs: delta
Constructing Itertaors
To make an object compatible with for loops and other iteration tools, it must conform to the iterator protocol. This requires two methods: __iter__, which returns the iterator object (often self), and __next__, which yields sequential values until exhaustion. Once the sequence ends, __next__ must raise StopIteration.
class ReverseIterator:
def __init__(self, collection):
self.sequence = collection
self.current_index = len(collection) - 1
def __iter__(self):
return self
def __next__(self):
if self.current_index < 0:
raise StopIteration
value = self.sequence[self.current_index]
self.current_index -= 1
return value
numbers = [10, 20, 30]
iterator_obj = ReverseIterator(numbers)
for item in iterator_obj:
print(item)
# Outputs: 30, 20, 10 (each on a new line)
Defining String Representations
Custom classes often require tailored text output for logging, debugging, and user display. Python distinguishes between two string conversion methods. __repr__ aims for an unambiguous, developer-focused representation that ideally could be used to reconstruct the object. __str__ focuses on readability and is invoked by print() and str().
class Vector3D:
def __init__(self, x_coord, y_coord, z_coord):
self.x = x_coord
self.y = y_coord
self.z = z_coord
def __repr__(self):
return f"Vector3D({self.x}, {self.y}, {self.z})"
def __str__(self):
return f"[{self.x}, {self.y}, {self.z}]"
point = Vector3D(4.5, -2.1, 8.0)
print(repr(point)) # Outputs: Vector3D(4.5, -2.1, 8.0)
print(point) # Outputs: [4.5, -2.1, 8.0]