1
2 """
3 odict
4 ~~~~~
5
6 This module is an example implementation of an ordered dict for the
7 collections module. It's not written for performance (it actually
8 performs pretty bad) but to show how the API works.
9
10
11 Questions and Answers
12 =====================
13
14 Why would anyone need ordered dicts?
15
16 Dicts in python are unordered which means that the order of items when
17 iterating over dicts is undefined. As a matter of fact it is most of
18 the time useless and differs from implementation to implementation.
19
20 Many developers stumble upon that problem sooner or later when
21 comparing the output of doctests which often does not match the order
22 the developer thought it would.
23
24 Also XML systems such as Genshi have their problems with unordered
25 dicts as the input and output ordering of tag attributes is often
26 mixed up because the ordering is lost when converting the data into
27 a dict. Switching to lists is often not possible because the
28 complexity of a lookup is too high.
29
30 Another very common case is metaprogramming. The default namespace
31 of a class in python is a dict. With Python 3 it becomes possible
32 to replace it with a different object which could be an ordered dict.
33 Django is already doing something similar with a hack that assigns
34 numbers to some descriptors initialized in the class body of a
35 specific subclass to restore the ordering after class creation.
36
37 When porting code from programming languages such as PHP and Ruby
38 where the item-order in a dict is guaranteed it's also a great help
39 to have an equivalent data structure in Python to ease the transition.
40
41 Where are new keys added?
42
43 At the end. This behavior is consistent with Ruby 1.9 Hashmaps
44 and PHP Arrays. It also matches what common ordered dict
45 implementations do currently.
46
47 What happens if an existing key is reassigned?
48
49 The key is *not* moved. This is consitent with existing
50 implementations and can be changed by a subclass very easily::
51
52 class movingodict(odict):
53 def __setitem__(self, key, value):
54 self.pop(key, None)
55 odict.__setitem__(self, key, value)
56
57 Moving keys to the end of a ordered dict on reassignment is not
58 very useful for most applications.
59
60 Does it mean the dict keys are sorted by a sort expression?
61
62 That's not the case. The odict only guarantees that there is an order
63 and that newly inserted keys are inserted at the end of the dict. If
64 you want to sort it you can do so, but newly added keys are again added
65 at the end of the dict.
66
67 I initializes the odict with a dict literal but the keys are not
68 ordered like they should!
69
70 Dict literals in Python generate dict objects and as such the order of
71 their items is not guaranteed. Before they are passed to the odict
72 constructor they are already unordered.
73
74 What happens if keys appear multiple times in the list passed to the
75 constructor?
76
77 The same as for the dict. The latter item overrides the former. This
78 has the side-effect that the position of the first key is used because
79 the key is actually overwritten:
80
81 >>> odict([('a', 1), ('b', 2), ('a', 3)])
82 odict.odict([('a', 3), ('b', 2)])
83
84 This behavor is consistent with existing implementation in Python
85 and the PHP array and the hashmap in Ruby 1.9.
86
87 This odict doesn't scale!
88
89 Yes it doesn't. The delitem operation is O(n). This is file is a
90 mockup of a real odict that could be implemented for collections
91 based on an linked list.
92
93 Why is there no .insert()?
94
95 There are few situations where you really want to insert a key at
96 an specified index. To now make the API too complex the proposed
97 solution for this situation is creating a list of items, manipulating
98 that and converting it back into an odict:
99
100 >>> d = odict([('a', 42), ('b', 23), ('c', 19)])
101 >>> l = d.items()
102 >>> l.insert(1, ('x', 0))
103 >>> odict(l)
104 odict.odict([('a', 42), ('x', 0), ('b', 23), ('c', 19)])
105
106 :copyright: (c) 2008 by Armin Ronacher and PEP 273 authors.
107 :license: modified BSD license.
108 """
109 from itertools import izip, imap
110 from copy import deepcopy
111
112 missing = object()
113
114
115 -class odict(dict):
116 """
117 Ordered dict example implementation.
118
119 This is the proposed interface for a an ordered dict as proposed on the
120 Python mailinglist (proposal_).
121
122 It's a dict subclass and provides some list functions. The implementation
123 of this class is inspired by the implementation of Babel but incorporates
124 some ideas from the `ordereddict`_ and Django's ordered dict.
125
126 The constructor and `update()` both accept iterables of tuples as well as
127 mappings:
128
129 >>> d = odict([('a', 'b'), ('c', 'd')])
130 >>> d.update({'foo': 'bar'})
131 >>> d
132 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
133
134 Keep in mind that when updating from dict-literals the order is not
135 preserved as these dicts are unsorted!
136
137 You can copy an odict like a dict by using the constructor, `copy.copy`
138 or the `copy` method and make deep copies with `copy.deepcopy`:
139
140 >>> from copy import copy, deepcopy
141 >>> copy(d)
142 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
143 >>> d.copy()
144 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
145 >>> odict(d)
146 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
147 >>> d['spam'] = []
148 >>> d2 = deepcopy(d)
149 >>> d2['spam'].append('eggs')
150 >>> d
151 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])])
152 >>> d2
153 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', ['eggs'])])
154
155 All iteration methods as well as `keys`, `values` and `items` return
156 the values ordered by the the time the key-value pair is inserted:
157
158 >>> d.keys()
159 ['a', 'c', 'foo', 'spam']
160 >>> d.values()
161 ['b', 'd', 'bar', []]
162 >>> d.items()
163 [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]
164 >>> list(d.iterkeys())
165 ['a', 'c', 'foo', 'spam']
166 >>> list(d.itervalues())
167 ['b', 'd', 'bar', []]
168 >>> list(d.iteritems())
169 [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]
170
171 Index based lookup is supported too by `byindex` which returns the
172 key/value pair for an index:
173
174 >>> d.byindex(2)
175 ('foo', 'bar')
176
177 You can reverse the odict as well:
178
179 >>> d.reverse()
180 >>> d
181 odict.odict([('spam', []), ('foo', 'bar'), ('c', 'd'), ('a', 'b')])
182
183 And sort it like a list:
184
185 >>> d.sort(key=lambda x: x[0].lower())
186 >>> d
187 odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])])
188
189 .. _proposal: http://thread.gmane.org/gmane.comp.python.devel/95316
190 .. _ordereddict: http://www.xs4all.nl/~anthon/Python/ordereddict/
191 """
192
194 dict.__init__(self)
195 self._keys = []
196 self.update(*args, **kwargs)
197
201
203 if key not in self:
204 self._keys.append(key)
205 dict.__setitem__(self, key, item)
206
208 if memo is None:
209 memo = {}
210 d = memo.get(id(self), missing)
211 if d is not missing:
212 return d
213 memo[id(self)] = d = self.__class__()
214 dict.__init__(d, deepcopy(self.items(), memo))
215 d._keys = self._keys[:]
216 return d
217
219 return {'items': dict(self), 'keys': self._keys}
220
222 self._keys = d['keys']
223 dict.update(d['items'])
224
226 return reversed(self._keys)
227
229 if isinstance(other, odict):
230 if not dict.__eq__(self, other):
231 return False
232 return self.items() == other.items()
233 return dict.__eq__(self, other)
234
236 return not self.__eq__(other)
237
239 if isinstance(other, odict):
240 return cmp(self.items(), other.items())
241 elif isinstance(other, dict):
242 return dict.__cmp__(self, other)
243 return NotImplemented
244
245 @classmethod
246 - def fromkeys(cls, iterable, default=None):
247 return cls((key, default) for key in iterable)
248
250 del self._keys[:]
251 dict.clear(self)
252
254 return self.__class__(self)
255
257 return zip(self._keys, self.values())
258
261
264
266 return iter(self._keys)
267
269 if default is missing:
270 return dict.pop(self, key)
271 elif key not in self:
272 return default
273 self._keys.remove(key)
274 return dict.pop(self, key, default)
275
279
281 if key not in self:
282 self._keys.append(key)
283 dict.setdefault(self, key, default)
284
285 - def update(self, *args, **kwargs):
286 sources = []
287 if len(args) == 1:
288 if hasattr(args[0], 'iteritems'):
289 sources.append(args[0].iteritems())
290 else:
291 sources.append(iter(args[0]))
292 elif args:
293 raise TypeError('expected at most one positional argument')
294 if kwargs:
295 sources.append(kwargs.iteritems())
296 for iterable in sources:
297 for key, val in iterable:
298 self[key] = val
299
301 return map(self.get, self._keys)
302
304 return imap(self.get, self._keys)
305
307 return self._keys.index(item)
308
310 key = self._keys[item]
311 return (key, dict.__getitem__(self, key))
312
315
316 - def sort(self, *args, **kwargs):
317 self._keys.sort(*args, **kwargs)
318
320 return 'odict.odict(%r)' % self.items()
321
322 __copy__ = copy
323 __iter__ = iterkeys
324
325
326 if __name__ == '__main__':
327 import doctest
328 doctest.testmod()
329