How would I create a custom list class in python?

Viewed 68487

I would like to write a custom list class in Python (let's call it MyCollection) where I can eventually call:

for x in myCollectionInstance:
    #do something here

How would I go about doing that? Is there some class I have to extend, or are there any functions I must override in order to do so?

5 Answers

In Python 3 we have beautiful collections.UserList([list]):

Class that simulates a list. The instance’s contents are kept in a regular list, which is accessible via the data attribute of UserList instances. The instance’s contents are initially set to a copy of list, defaulting to the empty list []. list can be any iterable, for example a real Python list or a UserList object.

In addition to supporting the methods and operations of mutable sequences, UserList instances provide the following attribute: data A real list object used to store the contents of the UserList class.

https://docs.python.org/3/library/collections.html#userlist-objects

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.

Related