पाठ 23 / 25

Building Custom Containers

Implement Pythonic containers with dunder methods and collections.abc.

Containers that feel built in

Python's data model lets your classes behave like built-in containers by implementing special (dunder) methods: __len__ (for len()), __iter__ (for loops and unpacking), __contains__ (for in), __getitem__, __setitem__ and __delitem__ (for indexing and slicing), __repr__ (for debugging) and __eq__. The collections.abc module defines abstract base classes such as Sequence, MutableSequence, Mapping, MutableMapping and Set: inherit from one, implement the few required abstract methods, and you get the rest (such as get, keys, items, __contains__ and index) for free, plus correct isinstance checks. For type hints, use generic types (class Ring[T] with Python 3.12 syntax, or Generic[T] in older versions) and protocols from typing (Iterable, Sized) to accept any object with the right shape. Good custom containers validate input, document complexity, keep invariants private (prefixing internals with an underscore) and raise the same exceptions built-ins do (IndexError, KeyError).

A fixed-size ring buffer as a MutableSequence-like container

Dunder methods plus collections.abc for free extras.

from collections.abc import Sequence
from typing import Iterator

class RingBuffer[T](Sequence[T]):                  # Python 3.12+ generic syntax
    """Keeps the most recent `capacity` items; O(1) append and indexing."""

    def __init__(self, capacity: int) -> None:
        if capacity <= 0:
            raise ValueError("capacity must be positive")
        self._data: list[T | None] = [None] * capacity
        self._start = 0
        self._size = 0

    def append(self, item: T) -> None:
        end = (self._start + self._size) % len(self._data)
        self._data[end] = item
        if self._size < len(self._data):
            self._size += 1
        else:
            self._start = (self._start + 1) % len(self._data)   # overwrite the oldest

    def __len__(self) -> int:
        return self._size

    def __getitem__(self, index):                  # Sequence requires __getitem__ and __len__
        if isinstance(index, slice):
            return [self[i] for i in range(*index.indices(self._size))]
        if index < 0:
            index += self._size
        if not 0 <= index < self._size:
            raise IndexError("ring buffer index out of range")
        return self._data[(self._start + index) % len(self._data)]

    def __repr__(self) -> str:
        return f"RingBuffer({list(self)!r}, capacity={len(self._data)})"

temps = RingBuffer[float](3)
for t in [21.5, 22.0, 23.1, 24.4]:
    temps.append(t)
print(temps)                 # RingBuffer([22.0, 23.1, 24.4], capacity=3)
print(temps[-1], temps[:2])  # 24.4 [22.0, 23.1]
print(23.1 in temps, temps.index(24.4))   # True 2: provided free by Sequence

Fitting a standard socket

Implementing the dunder methods is like fitting a standard plug to your appliance: once it fits, every socket in the house (for loops, len, in, slicing, library functions) works with it.

त्वरित जाँच: If you subclass collections.abc.Sequence and implement __getitem__ and __len__, what do you get for free?

  • Nothing
  • Methods such as __contains__, __iter__, index, count and __reversed__
  • Automatic sorting
  • Thread safety
Answer

Methods such as __contains__, __iter__, index, count and __reversed__ — The ABC provides mixin methods built on the abstract ones.