Slicing a Python OrderedDict

2019-02-06 04:01发布

问题:

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?

回答1:

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]


回答2:

You can use the itertools.islice function, which takes an iterable and outputs the stop first elements. This is beneficial since iterables don't support the common slicing method, and you won't need to create the whole items list from the OrderedDict.

from collections import OrderedDict
from itertools import islice
o = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
sliced = islice(o.iteritems(), 3)  # o.iteritems() is o.items() in Python 3
sliced_o = OrderedDict(sliced)


回答3:

In Python 2, you can slice the keys:

x.keys()[1:3]

and to support both Python 2 and Python 3, you'd convert to a list first:

list(k)[1:3]

The Python 2 OrderedDict.keys() implementation does exactly that.

In both cases you are given a list of keys in correct order. If creating a whole list first is an issue, you can use itertools.islice() and convert the iterable it produces to a list:

from itertools import islice

list(islice(x, 1, 3))

All of the above also can be applied to the items; use dict.viewitems() in Python 2 to get the same iteration behaviour as Python 3 dict.items() provides. You can pass the islice() object straight to another OrderedDict() in this case:

OrderedDict(islice(x.items(), 1, 3))  # x.viewitems() in Python 2


回答4:

I wanted to slice using a key, since I didn't know the index in advance:

o = OrderedDict(zip(list('abcdefghijklmnopqrstuvwxyz'),range(1,27)))

stop = o.keys().index('e')           # -> 4
OrderedDict(islice(o.items(),stop))  # -> OrderedDict([('a', 1), ('b', 2), ('c', 3)])

or to slice from start to stop:

start = o.keys().index('c')                    # -> 2
stop = o.keys().index('e')                     # -> 4
OrderedDict(islice(o.iteritems(),start,stop))  # -> OrderedDict([('c', 3), ('d', 4)])


回答5:

def slice_odict(odict, start=None, end=None):
    return OrderedDict([
        (k,v) for (k,v) in odict.items() 
        if k in list(odict.keys())[start:end]
    ])

This allows for:

>>> x = OrderedDict([('a',1), ('b',2), ('c',3), ('d',4)])
>>> slice_odict(x, start=-1)
OrderedDict([('d', 4)])
>>> slice_odict(x, end=-1)
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> slice_odict(x, start=1, end=3)
OrderedDict([('b', 2), ('c', 3)])