Implementing a list from scratch requires you to implement
the full container protocol:
__len__()
__iter__() __reversed__()
_getitem__() __contains__()
__setitem__() __delitem__()
__eq__() __ne__() __gt__()
__lt__() __ge__() __le__()
__add__() __radd__() __iadd__()
__mul__() __rmul__() __imul__()
__str__() __repr__() __hash__
But the crux of the list is its read-only protocol,
as captured by collections.abc.Sequence
's 3 methods:
__len__()
__getitem__()
__iter__()
To see that in action, here it is a lazy read-only list backed
by a range
instance
(super handy because it knows how to do slicing gymnastics),
where any materialized values are stored in a cache (e.g. a dictionary):
import copy
from collections.abc import Sequence
from typing import Dict, Union
class LazyListView(Sequence):
def __init__(self, length):
self._range = range(length)
self._cache: Dict[int, Value] = {}
def __len__(self) -> int:
return len(self._range)
def __getitem__(self, ix: Union[int, slice]) -> Value:
length = len(self)
if isinstance(ix, slice):
clone = copy.copy(self)
clone._range = self._range[slice(*ix.indices(length))] # slicing
return clone
else:
if ix < 0:
ix += len(self) # negative indices count from the end
if not (0 <= ix < length):
raise IndexError(f"list index {ix} out of range [0, {length})")
if ix not in self._cache:
... # update cache
return self._cache[ix]
def __iter__(self) -> dict:
for i, _row_ix in enumerate(self._range):
yield self[i]
Although the above class is still missing the write-protocol and all the rest methods
like __eq__()
, __add__()
, it is already quite functional.
>>> alist = LazyListView(12)
>>> type(alist[3:])
LazyListView
A nice thing is that slices retain the class, so they refrain breaking laziness
and materialize elements (e.g. by coding an appropriate repr()
method).
Yet the class still fails miserably in simple tests:
>>> alist == alist[:]
False
You have to implement __eq__()
to fix this, and use facilities like
functools.total_ordering()
to implement __gt__()
etc:
from functools import total_ordering
@total_ordering
class LaxyListView
def __eq__(self, other):
if self is other:
return True
if len(self) != len(other):
return False
return all(a == b for a, b in zip(self, other)
def __lt__(self, other):
if self is other:
return 0
res = all(self < other for a, b in zip(self, other)
if res:
return len(self) < len(other)
But that is indeed considerable effort.
NOTICE: if you try to bypass the effort and inherit list
(instead of Sequence
),
more modifications are needed because, e.g. copy.copy()
would now try to copy also
the underlying list and end up calling __iter__()
, destroying laziness;
furthermore, __add__()
method fills-in internally list, breaking adding of slices.