18

I'm trying to create a list equivalent for the very useful collections.defaultdict. The following design works nicely:

class defaultlist(list):
    def __init__(self, fx):
        self._fx = fx
    def __setitem__(self, index, value):
        while len(self) <= index:
            self.append(self._fx())
        list.__setitem__(self, index, value)

Here's how you use it:

>>> dl = defaultlist(lambda:'A')
>>> dl[2]='B'
>>> dl[4]='C'
>>> dl
['A', 'A', 'B', 'A', 'C']

What should I add to the defaultlist so as to support the following behavior?

>>> dl = defaultlist(dict)
>>> dl[2]['a'] = 1
>>> dl
[{}, {}, {'a':1}]
Jonathan Livni
  • 101,334
  • 104
  • 266
  • 359

4 Answers4

24

On the example you give, you first try to retrieve a non-existing value on the list, as you do dl[2]['a'], Python first retrieve the third (index 2) element on the list, then proceed to get the element named 'a' on that object - therefore you have to implement your automatic extending behavior to the __getitem__ method as well, like this:

class defaultlist(list):
    def __init__(self, fx):
        self._fx = fx
    def _fill(self, index):
        while len(self) <= index:
            self.append(self._fx())
    def __setitem__(self, index, value):
        self._fill(index)
        list.__setitem__(self, index, value)
    def __getitem__(self, index):
        self._fill(index)
        return list.__getitem__(self, index)
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • I also hoped something simple like this would be enough, but keep in mind that index could also be a slice. For example `lst = defaultlist(lambda:None); lst[:]` will throw `TypeError: '<=' not supported between instances of 'int' and 'slice'` – xaedes Dec 18 '22 at 18:19
  • To also support slices I used something like this in my code: `idx = (idx if type(idx) is int else ((idx.stop if idx.stop is not None else len(lst))-(idx.step if idx.step is not None else 1)) if type(idx) is slice else len(lst)-1)` – xaedes Dec 18 '22 at 18:35
15

There is a python package available:

$ pip install defaultlist

Added indicies are filled with None by default.

>>> from defaultlist import defaultlist
>>> l = defaultlist()
>>> l
[]
>>> l[2] = "C"
>>> l
[None, None, 'C']
>>> l[4]
>>> l
[None, None, 'C', None, None]

Slices and negative indicies are supported likewise

>>> l[1:4]
[None, 'C', None]
>>> l[-3]
'C'

Simple factory functions can be created via lambda.

>>> l = defaultlist(lambda: 'empty')
>>> l[2] = "C"
>>> l[4]
'empty'
>>> l
['empty', 'empty', 'C', 'empty', 'empty']

It is also possible to implement advanced factory functions:

>>> def inc():
...     inc.counter += 1
...     return inc.counter
>>> inc.counter = -1
>>> l = defaultlist(inc)
>>> l[2] = "C"
>>> l
[0, 1, 'C']
>>> l[4]
4
>>> l
[0, 1, 'C', 3, 4]

See the Documentation for any further details.

c0fec0de
  • 651
  • 8
  • 4
  • ModuleNotFoundError: No module named 'defaultlist' Which is weird because pip freeze shows that 1.0 is installed. – rjurney Jan 14 '21 at 19:49
1

One could implement a defaultlist that inherits from the MutableSequence abstract base class and wraps around the defaultdict. I do so in my coinflip package and expose it in the coinflip.collections submodule.

One would need to override the ABC like so:

class defaultlist(MutableSequence):
    def __getitem__(self, i):
        ...

    def __setitem__(self, i, value):
        ...

    def __delitem__(self, i):
        ...

    def __len__(self):
        ...
        
    def insert(self):
        ...

I would initialise this defaultlist by mirroring the defaultdict(default_factory=None) method, passing default_factory to an internal private defaultdict.

Like with c0fec0de's solution, I would recommend filling the indices with None by default (i.e. pass in a "none factory" method), otherwise you will get a KeyError using unaccessed indices.

The accessor methods (get, set and delete items) would have indices just be keys in the private defaultdict, and update that dictionary accordingly when items are added and/or removed.

There are lesser-used aspects of the builtin list which, if one wishes to emulate, need some special considerations. I wrote more in-depth about the subject here.

honno
  • 53
  • 1
  • 6
-4

One really simple way to do a defaultlist is with with a list comprehension and a range.

defaultvalue = "catfood"
x = [defaultvalue for i in range(0,4)]

which would result in

['catfood', 'catfood', 'catfood', 'catfood']

This could be a subclass, but doesn't need to be. It could just be a factory function like this:

def defaultlist(defaultvalue, elementcount):
    return [defaultvalue for i in range(0, elementcount)]

and would work like this

defaultlist(None, 7)

returning this

[None, None, None, None, None, None, None]
uchuugaka
  • 12,679
  • 6
  • 37
  • 55