Python OrderedDict iteration

Question:

Why does my python OrderedDict get initialized ‘out of order’?

The solution here is less intriguing than the explanation. There’s something here I just don’t get, and perhaps an expanation would help others as well as me.

>>> from collections import OrderedDict

>>> spam = OrderedDict(s = (1, 2), p = (3, 4), a = (5, 6), m = (7, 8))

>>> spam
OrderedDict([('a', (5, 6)), ('p', (3, 4)), ('s', (1, 2)), ('m', (7, 8))])

>>> for key in spam.keys():
...    print key    
...
#  this is 'ordered' but not the order I wanted....
a
p
s
m

# I was expecting (and wanting):
s
p
a
m
Asked By: neil.millikin

||

Answers:

From the docs:

The OrderedDict constructor and update() method both accept keyword arguments, but their order is lost because Python’s function call semantics pass-in keyword arguments using a regular unordered dictionary.

So initialization loses the ordering, because it’s basically calling a constructor with **kwargs.

Edit: In terms of a solution (not just an explanation)—as pointed out in a comment by the OP, passing in a single list of tuples will work:

>>> from collections import OrderedDict
>>> spam = OrderedDict([('s',(1,2)),('p',(3,4)),('a',(5,6)),('m',(7,8))])
>>> for key in spam:
...     print(key)
...
s
p
a
m
>>> for key in spam.keys():
...     print(key)
...
s
p
a
m

This is because it’s only getting a single argument, a list.

Answered By: Chris Krycho

@Chris Krycho gave a good explanation of why things fail.

If you look at the repr() of an OrderedDict you get a hint at how to impart order from the beginning: You need to use a list of (key, value) pairs to preserve the order of the keys given by the list.

Here’s one I did earlier:

>>> from collections import OrderedDict
>>> spamher = OrderedDict(s=6, p=5, a=4, m=3, h=2, e=1, r=0)
>>> spamher
OrderedDict([('h', 2), ('m', 3), ('r', 0), ('s', 6), ('p', 5), ('a', 4), ('e', 1)])
>>> 
>>> list(spamher.keys())
['h', 'm', 'r', 's', 'p', 'a', 'e']
>>> 
>>> spamher = OrderedDict([('s', 6), ('p', 5), ('a', 4), ('m', 3), ('h', 2), ('e', 1), ('r', 0)])
>>> list(spamher.keys())
['s', 'p', 'a', 'm', 'h', 'e', 'r']
>>> 

(It just so happened that in Python v3.3.0 your original example of spam kept the keys in their original order from the outset. I changed to spamher to get arounf this).

Answered By: Paddy3118

As the other answers have mentioned, trying to pass a dict to OrderedDict or using keyword arguments doesn’t preserve the order. Passing in tuples is kinda ugly, though, and this is Python. It should be beautiful.

You can abuse __getitem__ on a class in order to have dict-like syntax for creating OrderedDict “literals”:

from collections import OrderedDict
class OD(object):
    """This class provides a nice way to create OrderedDict "literals"."""
    def __getitem__(self, slices):
        if not isinstance(slices, tuple):
            slices = slices,
        return OrderedDict((slice.start, slice.stop) for slice in slices)
# Create a single instance; we don't ever need to refer to the class.
OD = OD()

Now you can use dict-like syntax to create an OrderedDict:

spam = OD['s': (1, 2), 
          'p': (3, 4), 
          'a': (5, 6), 
          'm': (7, 8)]
assert(''.join(spam.keys()) == 'spam')

This works because inside the square brackets, Python creates slice literals, which happen to look like dict syntax if you squint a little.

The OD class could benefit from error checking, but this demonstrates how it can work.

Answered By: Cody Piersall
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.