Search code examples
python-2.7sliceordereddictionary

Slicing a Python OrderedDict


In my code I frequently need to take a subset range of keys+values from a Python OrderedDict (from collections package). Slicing doesn't work (throws TypeError: unhashable type) and the alternative, iterating, is cumbersome:

from collections import OrderedDict

o = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])

# want to do:
# x = o[1:3]
# need to do:
x = OrderedDict()
for idx, key in enumerate(o):
    if 1 <= idx < 3:
        x[key] = o[key]

Is there a better way to get this done?


Solution

  • The ordered dict in the standard library, doesn't provide that functionality. Even though libraries existed for a few years before collections.OrderedDict that have this functionality (and provide essentially a superset of OrderedDict): voidspace odict and ruamel.ordereddict (I am the author of the latter package, which is a reimplementation of odict in C):

    from odict import OrderedDict as odict
    p = odict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
    print p[1:3]
    

    In ruamel.ordereddict you can relax the ordered input requirement (AFAIK you cannot ask derivative of dict if its keys are ordered (would be good addition to ruamel.ordereddict to recognise collection.OrderedDicts)):

    from ruamel.ordereddict import ordereddict
    
    q = ordereddict(o, relax=True)
    print q[1:3]
    r = odict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
    print r[1:3]
    

    If you want (or have to) stay within the standard library you can sublass collections.OrderedDict's __getitem__:

    class SlicableOrderedDict(OrderedDict):
        def __getitem__(self, k):
            if not isinstance(k, slice):
                return OrderedDict.__getitem__(self, k)
            x = SlicableOrderedDict()
            for idx, key in enumerate(self.keys()):
                if k.start <= idx < k.stop:
                    x[key] = self[key]
            return x
    
    s = SlicableOrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
    print s[1:3]
    

    of course you could use Martijn's or Jimmy's shorter versions to get the actual slice that needs returning:

    from itertools import islice
    class SlicableOrderedDict(OrderedDict):
        def __getitem__(self, k):
            if not isinstance(k, slice):
                return OrderedDict.__getitem__(self, k)
            return SlicableOrderedDict(islice(self.viewitems(), k.start, k.stop))
    
    t = SlicableOrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
    print t[1:3]
    

    or if you just want smarten up all existing OrderedDicts without subclassing:

    def get_item(self, k):
        if not isinstance(k, slice):
            return OrderedDict._old__getitem__(self, k)
        return OrderedDict(islice(self.viewitems(), k.start, k.stop))
    
    OrderedDict._old__getitem__ = OrderedDict.__getitem__
    OrderedDict.__getitem__ = get_item
    
    u = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
    print u[1:3]