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
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.
@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).
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.
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
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.
@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).
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.