[Python-ideas] Yet More Unpacking Generalizations (or, Dictionary Literals as lvalues)
Steven D'Aprano
steve at pearwood.info
Thu Aug 13 04:41:41 CEST 2015
On Wed, Aug 12, 2015 at 11:44:05AM -0700, Scott Sanderson wrote:
> > I think the sequence unpacking version above reads much better than
> > this hypothetical dict unpacking version:
> > {'foo': foo, 'bar': bar, 'baz': baz, 'spam': spam,
> > 'eggs': eggs, 'cheese': cheese} = json.loads(json_dict)
>
>
> As with many things in Python, I think that how you format this expression
> makes a big difference. I'd write it like this:
>
> {
> 'foo': foo,
> 'bar': bar,
> 'baz': baz,
> 'spam': spam,
> 'eggs': eggs,
> 'cheese': cheese,
> } = json.loads(json_dict)
That's still awfully verbose, and not much of a saving from:
foo = d['foo']
bar = d['bar']
baz = d['baz']
etc. You save a little bit of typing, but not that much.
> I prefer this to your example of unpacking from a list comprehension
> because I think it does a better job of expressing to a reader the expected
> structure of the input data.
I don't think it does. I think the above would be incomprehensible to
somebody who hasn't learned the details of this. It looks like you are
creating a dict, but not assigning the dict to anything. And where do
the unquoted values foo, bar, etc. come from? They look like they should
come from already existing local variables:
{'foo': foo}
as an expression (rather than an assignment target) requires an existing
foo variable (otherwise you get a NameError). So the behaviour has to be
learned, it isn't something that the reader can extrapolate from other
assignment syntax.
It isn't obvious what this does:
{foo: bar} = some_dict
because there's no one-to-one correspondence between assignment target
and assignment name. With sequence unpacking, the targets are obvious:
foo, bar, baz = ...
clearly has assignment targets foo, bar and baz. What else could they
be? It's easy to extrapolate it from single assignment foo = ...
But with your syntax, you have keys and values, and it isn't clear what
gets used for what. The dict display form doesn't look like any other
assignment target, you have to learn it as a special case. A reader
who hasn't learned the rules could be forgiven for guessing any of the
following rules:
(1) create a variable foo from existing variable bar
(2) create a variable foo from some_dict['bar']
(3) create a variable with the name given by the value of foo, from
some_dict['bar']
(4) create a variable bar from some_dict['foo']
(5) create a variable with the name given by the value of bar, from
some_dict['foo']
and others.
You could make that a bit more clear by requiring the keys to be quoted,
so {foo: bar} = ... would be illegal, and you have to write {'foo':
'bar'}, but that's annoying.
Or we could go the other way and not quote anything: {foo: bar} = d
could create variable foo from d['bar']. That's not bad looking, and
avoids all the quote marks, but I don't think people would guess that's
the behaviour. It still doesn't look like an assignment target.
And the common case is still verbose: {foo: foo} = ...
What if we have expressions in there?
{foo.upper() + 's': bar} = some_dict
{foo: bar or baz} = some_dict
I would hope both of those are syntax errors! But maybe somebody will
want them. At least, some people will expect them, because that sort of
thing works in dict displays. You even hint at arbitrary values below,
with a tuple (baz_x, baz_y).
> It's also much easier to modify this to
> extract nested values, ala:
>
> {
> 'foo': foo,
> 'bar': bar,
> 'baz': (baz_x, baz_y),
> 'spam': spam,
> 'eggs': eggs,
> 'cheese': cheese,
> } = json.loads(json_dict)
So baz is a tuple of d['baz_x'], d['baz_y']?
Does this mean you want to allow arbitrary expressions for the values?
{'foo': func(foo or bar.upper() + "s") + baz} = d
If so, what are the scoping rules? Which of func, foo, bar and baz are
looked up from the right-hand side dict, and which are taken from the
current scope?
I think allowing arbitrary expressions cannot work in any reasonable
manner, but special casing tuples (baz_x, baz_y) is too much of a
special case.
> More practical, and in the spirit of tuple unpacking:
> > spam, eggs, cheese = **expression
>
>
> I like how concise this syntax is. I'd be sad that it doesn't allow
> unpacking of nested expressions, though I think we disagree on whether
> that's actually an issue.
> A more substantial objection might be that this could only work on mapping
> objects with strings for keys.
Does that mean that you expect your syntax to support non-identifier key
lookups?
{'foo': 123, 'bar': 'x y', 'baz': None} = d
will look for keys 123 (or should that be '123'?), 'x y' and None (or
possibly 'None')? If so, I think you've over-generalised from a fairly
straightforward use-case:
unpack key:values in a mapping to variables with the
same name as the keys
and YAGNI applies. For cases where the keys are not the same as the
variables, or you want to use non-identifier keys, just use the good-old
fashioned form:
variable = d['some non-identifier']
--
Steve
More information about the Python-ideas
mailing list