ultraturtle0 ultraturtle0 - 24 days ago 6x
Python Question

Python OrderedDict __setitem__ overloading

I'm building a class that inherits OrderedDict, in which every key returns a list. I'm looking to overload setitem such that if the key doesn't exist, new assignments immediately put the value into a list, otherwise the new value is appended to the list. The following seems to be working:

from collections import OrderedDict

class ListDict(OrderedDict):
def __init__(self):
super(ListDict, self).__init__()

def __setitem__(self, key, value):
if key in self.keys():
super(ListDict, self).__setitem__(key, [value])

thedict = ListDict()

thedict['a'] = 'first item'
thedict['b'] = 'another first item'
thedict['a'] = 'a second item?'

print thedict

which prints:

$ python inheritex.py
ListDict([('a', ['first item', 'a second item?']), ('b', ['another first item'])])

Instead of appending with an assignment operator '=', I would rather have new items be appended with '=+', or even something like:

ListDict['key'] = ListDict['key'] + 'value'

How would one go about overloading this? Besides being possible to monkey patch an add function, is it even a Pythonic/readable means to alter class behavior, or is this acceptable because the inherited function (OrderedDict) is untouched?


You can already use += on existing keys:

>>> from collections import OrderedDict
>>> thedict = OrderedDict()
>>> thedict['a'] = []  # set initial list
>>> thedict['a'] += ['foo']
>>> thedict['a'] += ['bar']
>>> thedict
OrderedDict([('a', ['foo', 'bar'])])

Note that += on a list is basically the same thing as list.extend(), so you need to append lists.

If you want this to work for keys that don't exist yet, implement a __missing__ method, rather than __setitem__:

class ListDict(OrderedDict):
    def __missing__(self, key):
        self[key] = []
        return self[key]

When a key is missing during dict[key] lookup, __missing__ is called and it's return value is returned instead of raising a KeyError.

Now both + and += work on missing keys too:

>>> thedict = ListDict()
>>> thedict['a'] += ['foo', 'bar']
>>> thedict['b'] = thedict['b'] + ['spam', 'ham']
>>> thedict
ListDict([('a', ['foo', 'bar']), ('b', ['spam', 'ham'])])

If you concatenation must work without adding lists, you could produce a custom list subclass too:

class ConcatList(list):
    def __add__(self, value):
        return type(self)(super().__add__([value]))
    def __iadd__(self, value):
        return self

then use that type in __missing__:

class ListDict(OrderedDict):
    def __missing__(self, key):
        self[key] = ConcatList()
        return self[key]

after which you can forgo brackets:

>>> thedict = ListDict()
>>> thedict['a'] += 'foo'
>>> thedict['b'] = thedict['b'] + 'bar'
>>> thedict
ListDict([('a', ['foo']), ('b', ['bar'])])