Container Protocol¶
Container dunder methods enable your objects to behave like built-in collections.
Core Container Methods¶
| Method | Called By | Description |
|---|---|---|
__len__ |
len(obj) |
Return number of items |
__getitem__ |
obj[key] |
Get item by key/index |
__setitem__ |
obj[key] = value |
Set item by key/index |
__delitem__ |
del obj[key] |
Delete item by key/index |
__contains__ |
item in obj |
Check membership |
len: Collection Length¶
class Playlist:
def __init__(self, songs=None):
self._songs = songs or []
def __len__(self):
return len(self._songs)
def add(self, song):
self._songs.append(song)
playlist = Playlist(['Song A', 'Song B', 'Song C'])
print(len(playlist)) # 3
# Also enables bool() if __bool__ not defined
empty = Playlist()
if not empty:
print("Playlist is empty") # Prints this
getitem: Item Access¶
Index-Based Access¶
class Sentence:
def __init__(self, text):
self._words = text.split()
def __getitem__(self, index):
return self._words[index]
def __len__(self):
return len(self._words)
s = Sentence("Hello World from Python")
print(s[0]) # Hello
print(s[-1]) # Python
print(s[1:3]) # ['World', 'from'] (slicing works!)
Key-Based Access¶
class Config:
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
def __contains__(self, key):
return key in self._data
config = Config()
config['debug'] = True
config['host'] = 'localhost'
print(config['debug']) # True
print('debug' in config) # True
print('missing' in config) # False
Handling Slices¶
class MyList:
def __init__(self, data):
self._data = list(data)
def __getitem__(self, index):
if isinstance(index, slice):
# Return a new MyList for slices
return MyList(self._data[index])
return self._data[index]
def __setitem__(self, index, value):
if isinstance(index, slice):
self._data[index] = value
else:
self._data[index] = value
def __repr__(self):
return f"MyList({self._data})"
lst = MyList([1, 2, 3, 4, 5])
print(lst[1:4]) # MyList([2, 3, 4])
print(lst[::2]) # MyList([1, 3, 5])
lst[1:3] = [20, 30]
print(lst) # MyList([1, 20, 30, 4, 5])
Multi-Dimensional Access¶
class Matrix:
def __init__(self, rows, cols):
self._data = [[0] * cols for _ in range(rows)]
self._rows = rows
self._cols = cols
def __getitem__(self, key):
if isinstance(key, tuple):
row, col = key
return self._data[row][col]
# Single index returns entire row
return self._data[key]
def __setitem__(self, key, value):
if isinstance(key, tuple):
row, col = key
self._data[row][col] = value
else:
self._data[key] = value
def __repr__(self):
return f"Matrix({self._data})"
m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 5
m[2, 2] = 9
print(m[1, 1]) # 5
print(m[0]) # [1, 0, 0] (entire row)
setitem: Item Assignment¶
class DefaultDict:
def __init__(self, default_factory):
self._data = {}
self._default = default_factory
def __getitem__(self, key):
if key not in self._data:
self._data[key] = self._default()
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
def __repr__(self):
return f"DefaultDict({self._data})"
# Auto-create list for missing keys
dd = DefaultDict(list)
dd['fruits'].append('apple')
dd['fruits'].append('banana')
dd['vegetables'].append('carrot')
print(dd) # DefaultDict({'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})
delitem: Item Deletion¶
class Registry:
def __init__(self):
self._items = {}
def __setitem__(self, key, value):
self._items[key] = value
def __getitem__(self, key):
return self._items[key]
def __delitem__(self, key):
if key not in self._items:
raise KeyError(f"'{key}' not found")
del self._items[key]
def __contains__(self, key):
return key in self._items
def __repr__(self):
return f"Registry({self._items})"
reg = Registry()
reg['user1'] = 'Alice'
reg['user2'] = 'Bob'
print(reg) # Registry({'user1': 'Alice', 'user2': 'Bob'})
del reg['user1']
print(reg) # Registry({'user2': 'Bob'})
print('user1' in reg) # False
contains: Membership Test¶
class Range:
"""Efficient range membership testing."""
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __contains__(self, value):
return self.start <= value < self.stop
r = Range(1, 100)
print(50 in r) # True
print(100 in r) # False
print(0 in r) # False
Without contains¶
If __contains__ isn't defined, Python falls back to iteration:
class NoContains:
def __init__(self, data):
self._data = data
def __iter__(self):
return iter(self._data)
nc = NoContains([1, 2, 3])
print(2 in nc) # True (iterates through all items)
missing: Dict Subclass Hook¶
__missing__ is called by dict subclasses when a key isn't found.
class AutoDict(dict):
def __missing__(self, key):
# Auto-create nested dicts
self[key] = AutoDict()
return self[key]
d = AutoDict()
d['a']['b']['c'] = 42
print(d) # {'a': {'b': {'c': 42}}}
class CountingDict(dict):
def __init__(self):
super().__init__()
self.access_count = {}
def __missing__(self, key):
return 0 # Default value for missing keys
def __getitem__(self, key):
self.access_count[key] = self.access_count.get(key, 0) + 1
return super().__getitem__(key) if key in self else self.__missing__(key)
cd = CountingDict()
cd['a'] = 1
print(cd['a']) # 1
print(cd['a']) # 1
print(cd['b']) # 0 (missing, returns default)
print(cd.access_count) # {'a': 2, 'b': 1}
Practical Example: Sparse Matrix¶
class SparseMatrix:
"""Memory-efficient matrix that only stores non-zero values."""
def __init__(self, rows, cols, default=0):
self._data = {}
self.rows = rows
self.cols = cols
self.default = default
def _validate_key(self, key):
if not isinstance(key, tuple) or len(key) != 2:
raise TypeError("Index must be a tuple (row, col)")
row, col = key
if not (0 <= row < self.rows and 0 <= col < self.cols):
raise IndexError("Index out of bounds")
def __getitem__(self, key):
self._validate_key(key)
return self._data.get(key, self.default)
def __setitem__(self, key, value):
self._validate_key(key)
if value == self.default:
self._data.pop(key, None) # Don't store default values
else:
self._data[key] = value
def __delitem__(self, key):
self._validate_key(key)
self._data.pop(key, None)
def __contains__(self, key):
return key in self._data
def __len__(self):
return len(self._data) # Number of non-default values
def __repr__(self):
return f"SparseMatrix({self.rows}x{self.cols}, {len(self)} non-zero)"
# Usage
m = SparseMatrix(1000, 1000)
m[0, 0] = 1
m[500, 500] = 42
m[999, 999] = -1
print(m[0, 0]) # 1
print(m[1, 1]) # 0 (default)
print(len(m)) # 3 (only 3 stored values)
print((500, 500) in m) # True
print((1, 1) in m) # False
Sequence ABC Implementation¶
from collections.abc import MutableSequence
class TypedList(MutableSequence):
"""List that only accepts items of a specific type."""
def __init__(self, item_type, items=None):
self._type = item_type
self._data = []
if items:
for item in items:
self.append(item)
def _check_type(self, value):
if not isinstance(value, self._type):
raise TypeError(f"Expected {self._type.__name__}, got {type(value).__name__}")
def __getitem__(self, index):
return self._data[index]
def __setitem__(self, index, value):
self._check_type(value)
self._data[index] = value
def __delitem__(self, index):
del self._data[index]
def __len__(self):
return len(self._data)
def insert(self, index, value):
self._check_type(value)
self._data.insert(index, value)
def __repr__(self):
return f"TypedList[{self._type.__name__}]({self._data})"
# Usage
int_list = TypedList(int, [1, 2, 3])
int_list.append(4)
print(int_list) # TypedList[int]([1, 2, 3, 4])
# int_list.append("five") # TypeError: Expected int, got str
Mapping ABC Implementation¶
from collections.abc import MutableMapping
class CaseInsensitiveDict(MutableMapping):
"""Dictionary with case-insensitive string keys."""
def __init__(self, data=None):
self._data = {}
if data:
for key, value in data.items():
self[key] = value
def _normalize_key(self, key):
if isinstance(key, str):
return key.lower()
return key
def __getitem__(self, key):
return self._data[self._normalize_key(key)]
def __setitem__(self, key, value):
self._data[self._normalize_key(key)] = value
def __delitem__(self, key):
del self._data[self._normalize_key(key)]
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __repr__(self):
return f"CaseInsensitiveDict({self._data})"
# Usage
headers = CaseInsensitiveDict()
headers['Content-Type'] = 'application/json'
print(headers['content-type']) # application/json
print(headers['CONTENT-TYPE']) # application/json
print('content-type' in headers) # True
Key Takeaways¶
__len__enableslen()and boolean evaluation__getitem__enables indexing, slicing, and iteration fallback__setitem__enables item assignment with[]__delitem__enables item deletion withdel__contains__enablesinoperator (falls back to iteration)__missing__is a dict-specific hook for missing keys- Handle both integers and slices in
__getitem__for sequence types - Use
collections.abcbase classes for full protocol compliance - Tuples as keys enable multi-dimensional access:
obj[row, col]
Runnable Example: container_methods_tutorial.py¶
"""
Example 4: Container Magic Methods
Demonstrates: __len__, __getitem__, __setitem__, __delitem__, __contains__, __iter__
"""
class Playlist:
"""A custom playlist container."""
def __init__(self, name):
self.name = name
self.songs = []
def __repr__(self):
return f"Playlist('{self.name}', {len(self.songs)} songs)"
def __len__(self):
"""Return the number of songs in the playlist."""
return len(self.songs)
def __getitem__(self, index):
"""Get a song by index or slice."""
return self.songs[index]
def __setitem__(self, index, value):
"""Set a song at a specific index."""
self.songs[index] = value
def __delitem__(self, index):
"""Delete a song at a specific index."""
del self.songs[index]
def __contains__(self, song):
"""Check if a song is in the playlist."""
return song in self.songs
def __iter__(self):
"""Make the playlist iterable."""
return iter(self.songs)
def add_song(self, song):
"""Add a song to the playlist."""
self.songs.append(song)
class CustomDict:
"""A custom dictionary-like class."""
def __init__(self):
self._data = {}
def __repr__(self):
return f"CustomDict({self._data})"
def __len__(self):
"""Return number of items."""
return len(self._data)
def __getitem__(self, key):
"""Get value by key."""
if key not in self._data:
raise KeyError(f"Key '{key}' not found")
return self._data[key]
def __setitem__(self, key, value):
"""Set value by key."""
print(f"Setting {key} = {value}")
self._data[key] = value
def __delitem__(self, key):
"""Delete item by key."""
if key not in self._data:
raise KeyError(f"Key '{key}' not found")
del self._data[key]
def __contains__(self, key):
"""Check if key exists."""
return key in self._data
def __iter__(self):
"""Iterate over keys."""
return iter(self._data)
class Matrix:
"""A simple 2D matrix class."""
def __init__(self, rows, cols, default=0):
self.rows = rows
self.cols = cols
self._data = [[default for _ in range(cols)] for _ in range(rows)]
def __repr__(self):
return f"Matrix({self.rows}x{self.cols})"
def __str__(self):
"""Pretty print the matrix."""
lines = []
for row in self._data:
lines.append(" ".join(f"{val:6}" for val in row))
return "\n".join(lines)
def __getitem__(self, index):
"""Get item by [row, col] or [row]."""
if isinstance(index, tuple):
row, col = index
return self._data[row][col]
else:
return self._data[index]
def __setitem__(self, index, value):
"""Set item by [row, col] or [row]."""
if isinstance(index, tuple):
row, col = index
self._data[row][col] = value
else:
self._data[index] = value
def __len__(self):
"""Return number of rows."""
return self.rows
# Examples
if __name__ == "__main__":
# ============================================================================
print("=== Playlist Examples ===")
playlist = Playlist("My Favorites")
# Add songs
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
print(f"Playlist: {playlist}")
print(f"Length: {len(playlist)}")
# Access by index
print(f"\nFirst song: {playlist[0]}")
print(f"Last song: {playlist[-1]}")
# Slicing
print(f"First two songs: {playlist[0:2]}")
# Modify
playlist[1] = "Song B (Remix)"
print(f"Modified second song: {playlist[1]}")
# Check membership
print(f"\n'Song A' in playlist: {'Song A' in playlist}")
print(f"'Song Z' in playlist: {'Song Z' in playlist}")
# Iterate
print("\nAll songs:")
for i, song in enumerate(playlist, 1):
print(f" {i}. {song}")
# Delete
del playlist[2]
print(f"\nAfter deleting index 2: {len(playlist)} songs")
for song in playlist:
print(f" - {song}")
print("\n\n=== CustomDict Examples ===")
cd = CustomDict()
# Set items
cd["name"] = "Alice"
cd["age"] = 30
cd["city"] = "New York"
print(f"\nCustomDict: {cd}")
print(f"Length: {len(cd)}")
# Get items
print(f"\nName: {cd['name']}")
print(f"Age: {cd['age']}")
# Check membership
print(f"\n'name' in cd: {'name' in cd}")
print(f"'country' in cd: {'country' in cd}")
# Iterate
print("\nAll keys:")
for key in cd:
print(f" {key}: {cd[key]}")
# Delete
del cd["age"]
print(f"\nAfter deleting 'age': {cd}")
print("\n\n=== Matrix Examples ===")
matrix = Matrix(3, 3, default=0)
print(f"Matrix: {matrix}")
print(f"Length (rows): {len(matrix)}")
# Set values
matrix[0, 0] = 1
matrix[1, 1] = 5
matrix[2, 2] = 9
matrix[0, 2] = 3
print("\nMatrix after setting values:")
print(matrix)
# Get values
print(f"\nValue at [1, 1]: {matrix[1, 1]}")
print(f"First row: {matrix[0]}")
# Set entire row
matrix[1] = [2, 4, 6]
print("\nMatrix after setting row 1:")
print(matrix)