[Python-ideas] Dict literal use for custom dict classes
Steven D'Aprano
steve at pearwood.info
Sat Dec 12 22:24:16 EST 2015
On Sun, Dec 13, 2015 at 01:13:49AM +0100, Jelte Fennema wrote:
> I really like the OrderedDict class. But there is one thing that has always
> bothered me about it. Quite often I want to initialize a small ordered
> dict. When the keys are all strings this is pretty easy, since you can just
> use the keyword arguments. But when some, or all of the keys are other
> things this is an issue. In that case there are two options (as far as I
> know). If you want an ordered dict of this form for instance: {1: 'a', 4:
> int, 2: (3, 3)}, you would either have to use:
> OrderedDict([(1, 'a'), (4, int), (2, (3, 3))])
>
> or you could use:
> d = OrderedDict()
> d[1] = 'a'
> d[4] = int
> d[2] = (3, 3)
>
> In my opinion both are quite verbose and the first is pretty unreadable
> because of all the nested tuples.
You have a rather strict view of "unreadable" :-)
Some alternatives if you dislike the look of the above:
# Option 3:
d = OrderedDict()
for key, value in zip([1, 4, 2], ['a', int, (3, 3)]):
d[key] = value
# Option 4:
d = OrderedDict([(1, 'a'), (4, int)]) # The pretty values.
d[2] = (3, 3) # The ugly nested tuple at the end.
So there's no shortage of work-arounds for the lack of nice syntax for
creating ordered dicts.
And besides, why single out OrderedDict? There are surely people out
there using ordered mappings like RedBlackTree that have to deal with
this same issue. Perhaps what we need is to stop focusing on a specific
dictionary type, and think about the lowest common denominator for any
mapping. And that, I believe, is a list of (key, value) tuples. See
below.
> That is why I have two suggestions for
> language additions that fix that.
> The first one is the normal dict literal syntax available to custom dict
> classes like this:
> OrderedDict{1: 'a', 4: int, 2: (3, 3)}
I don't understand what that syntax is supposed to do.
Obviously it creates an OrderedDict, but you haven't explained the
details. Is the prefix "OrderedDict" hard-coded in the parser/lexer,
like the b prefix for byte-strings and r prefix for raw strings? In that
case, I think that's pretty verbose, and would prefer to see something
shorter:
o{1: 'a', 4: int, 2: (3, 3)}
perhaps. If OrderedDict is important enough to get its own syntax, it's
important enough to get its own *short* syntax. That was my preferred
solution, but it no longer is.
Or is the prefix "OrderedDict" somehow looked-up at run-time? So we
could write something like:
spam = random.choice(list_of_callables)
result = spam{1: 'a', 4: int, 2: (3, 3)}
and spam would be called, whatever it happens to be, with a single
list argument:
[(1, 'a'), (4, int), (2, (3, 3))]
What puts me off this solution is that it is syntactic sugar for not one
but two distinct operations:
- sugar for creating a list of tuples;
- and sugar for a function call.
But if we had the first, we don't need the second, and we don't need to
treat OrderedDict as a special case. We could use any mapping:
MyDict(sugar)
OrderedDict(sugar)
BinaryTree(sugar)
and functions that aren't mappings at all, but expect lists of (a, b)
tuples:
covariance(sugar)
> This looks much cleaner in my opinion. As far as I can tell it could simply
> be implemented as if the either of the two above options was used. This
> would make it available to all custom dict types that implement the two
> options above.
>
> A second very similar option, which might be cleaner and more useful, is to
> make this syntax available (only) after initialization. So it could be used
> like this:
> d = OrderedDict(){1: 'a', 4: int, 2: (3, 3)}
> d{3: 4, 'a': 'c'}
> *>>> *OrderedDict(){1: 'a', 4: int, 2: (3, 3), 3: 4, 'a': 'c'}
What does that actually do, in detail? Does it call d.__getitem__(key,
value) repeatedly? So I could do something like this:
L = [None]*10
L{1: 'a', 3: 'b', 5: 'c', 7: 'd', 9: 'e'}
assert L == [None, 'a', None, 'b', None, 'c', None, 'd', None, 'e']
If we had nice syntax for creating ordered dict literals, would we want
this feature? I don't think so. It must be pretty rare to want something
like that (at least, I can't remember the last time I did) and when we
do, we can often do it with slicing:
py> L = [None]*10
py> L[1::2] = 'abcde'
py> L
[None, 'a', None, 'b', None, 'c', None, 'd', None, 'e']
> This would allow arguments to the __init__ method as well.
How? You said that this option was only available after
initialization.
> And this way it could simply be a shorthand for setting multiple attributes.
How does the reader (or the interpreter) tell when
d{key: value}
means "call __setitem__" and when it means "call __setattr__"?
> It might even
> be used to change multiple values in a list if that is a feature that is
> wanted.
>
> Lastly I think either of the two sugested options could be used to allow
> dict comprehensions for custom dict types. But this might require a bit
> more work (although not much I think).
>
> I'm interested to hear what you guys think.
I think that there is a kernel of a good idea in this. Let's go back to
the idea of syntactic sugar for a list of tuples. The user can then call
the function or class of their choice, they aren't limited to just one
mapping type.
I'm going to suggest [key:value] as syntax. Now your original example
becomes:
d = OrderedDict([1: 'a', 4: int, 2: (3, 3)])
which breaks up the triple ))) at the end, so hopefully you will not
think its ugly. Also, we're not limited to just calling the constructor,
it could be any method:
d.update([2: None, 1: 'b', 5: 99.9])
or anywhere at all:
x = [2: None, 1: 'b', 5: 99.9, 1: 'a', 4: int, 2: (3, 3)] + items
# shorter and less error-prone than:
x = (
[(2, None), (1, 'b'), (5, 99.9), (1, 'a'), (4, int), (2, (3, 3))]
+ values
)
There could be a comprehension form:
[key: value for x in seq if condition]
similar to the dict comprehension form.
--
Steve
More information about the Python-ideas
mailing list