[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