On Sat, Apr 18, 2020 at 12:42:10AM -0700, Andrew Barnert wrote:

> > Inside function calls, the syntax
> > 
> >  **{identifier [, ...]}
> > 
> > expands to a set of `identifier=identifier` argument bindings.
> > 
> > This will be legal anywhere inside a function call that keyword 
> > unpacking would be legal.
> 
> Which means that you can’t just learn ** unpacking as a single 
> consistent thing that’s usable in multiple contexts with (almost) 
> identical syntax and identical meaning, you have to learn that it has 
> an additional syntax with a different meaning in just one specific 
> context, calls, that’s not legal in the others.

Um, yes? I think.

I'm afraid your objection is unclear to me. Obviously this would be one 
more thing to learn, but if the benefit is large enough, it would be 
worthwhile.

It would also be true whether we spell it using the initial suggestion, 
or using mode-shift, or by adding a new way to create dicts:

    f(meta, dunder=, reverse=)
    f(meta, *, dunder, reverse)
    f(meta, **{:dunder, :reverse})
    f(meta, **{dunder, reverse})

I'm not really sure I understand your comment about dict unpacking 
being "usable in multiple contexts with (almost) identical syntax and 
identical meaning". Can you give some examples?

I know that dict unpacking works in function calls:

    f(**d)

and I know it doesn't work in assignments:

    a, b = **d  # What would this even mean?

or in list-displays, etc. It *does* work in dict-displays:

    d = {'a': None, **mapping}

but I either didn't know it, or had forgotten it, until I tested it just 
now. (It quite surprised me too.) Are there any other contexts where 
this would work?

There's probably no reason why this keyword shortcut couldn't be allowed 
in dict-displays too:

   d = {'a': None, **{b, c, d})

or even as a new dict "literal":

   d = **{meta, dunder, reverse}

if there is call for it.

Personally, I would be conservative about allowing it in other contexts, 
as we can always add it later, but it's much harder to remove it if it 
were a mistake. This proposal is only about allowing it in a single 
context, function calls.


[...]
> Worse, this exact same syntax is a set display anywhere except in a ** 
> in a call.

You say "worse", I say "Better!"

It's a feature that this looks something like a set: you can read it 
as "unpack this set of identifiers as parameter:value arguments".

It's a feature that it uses the same `**` double star as dict unpacking: 
you can read it as unpacking a dict where the values are implied.

It is hardly unprecedented that things which look similar are not always 
identical, especially when dealing with something as basic as a 
sequence of terms in a comma-separated list:

    math, sys, functools, itertools, os

It's a tuple! Except when inside parentheses directly following an 
expression, or an import statement:

    import math, sys, functools, itertools, os
    obj.attribute.method(math, sys, functools, itertools, os)


> Not only is that another special case to learn about the 
> differences between set and dict displays, it also means that if you 
> naively copy and paste a subexpression from a call into somewhere else 
> (say, to print the value of that dict), you don’t get what you wanted, 
> or a syntax error, or even a runtime error, you get a perfectly valid 
> but very different value.

If you naively copy and paste the curly bracket part:

    f(meta, **{dunder, reverse})
    print({dunder, reverse})

you get to see the values in a set. Is that such a problem that it 
should kill the syntax?

There's a limit to how naive a user we need to care about in the 
language. We don't have to care about preventing every possible user 
error.


> > On the other hand, plain keyword unpacking:
> > 
> >  **textinfo
> > 
> > is terse, but perhaps too terse. Neither the keys nor the values are 
> > immediately visible. Instead, one must search the rest of the function 
> > or module for the definition of `textinfo` to learn which parameters are 
> > being filled in.
> 
> You can easily put the dict right before the call, and when you don’t, 
> it’s usually because there was a good reason.

Right. I'm not saying that dict unpacking is a usability disaster. I'm 
just pointing out that it separates the parameters from where they are 
being used. Yes, it could be one line away, or it could be buried deeply 
a thousand lines away, imported from another module, which you don't 
have the source code to.

I intentionally gave a real (or at least, real-ish) example using a real 
function from the standard library, and real parameter names. Without 
looking in the docs, can you tell what parameters are supplied by the 
`**textinfo` unpacking? I know I can't, and I wrote the damn thing! (I 
had to check the function signature to remind me what they were.)

Given:

    Popen( ..., **textinfo, ...)
    Popen( ..., **{encoding, errors, text}, ...)

I think that the second one is clearly superior in respect to showing 
the parameter names directly in place where they are used, while the 
first is clearly superior for brevity and terseness.


> And there are good reasons. Ideally you shouldn’t have any function 
> calls that are so hairy that you want to refractor them, but the the 
> existence of libraries you can’t control that are too huge and 
> unwieldy is the entire rationale here.

I wouldn't say the entire rationale.

For example, I have come across this a lot:

    def public_function(meta, reverse, private, dunder):
        do some pre-processing
        result = _private_function(
                     meta=meta, reverse=reverse, 
                     private=private, dunder=dunder
                     )
        do some post-processing
        return result


Changing the signature of `public_function` to take only a kwargs is not 
an option (for reasons I hope are obvious, but if not I'm happy to 
explain). Writing it like this instead just moves the pain without 
eliminating it:

        d = dict(meta=meta, reverse=reverse, 
                 private=private, dunder=dunder
                 )
        result = _private_function(**d)


So there's a genuine pain point here that regular keyword unpacking 
doesn't solve.

[...]
> > Backwards compatibility
> > -----------------------
> > 
> > The syntax is not currently legal so there are no backwards 
> > compatibility concerns.
> 
> The syntax is perfectly legal today.

Okay, I misspoke. Miswrote. It's not currently legal to use a set in 
dict unpacking.

I will re-iterate that this proposal does not construct a set. It just 
looks a bit like a set, in the same way that all of these have things 
which look like a bit like tuples but aren't:

    [a, b, c]
    func(a, b, c)
    import sys, os, collections
    except ValueError, TypeError, ImportError

and the same way that subscripting looks a bit like a list:

    mydict['key']  # Not actually a list ['key']


[...]
> Running Python 3.9 code in 3.8 would do the wrong thing, but maybe not 
> wrong enough to break your program visibly, which could lead to some 
> fun debugging sessions. That’s not a dealbreaker, but it’s definitely 
> better for new syntax to raise a syntax error in old versions, if 
> possible.

I don't think so. You would get a TypeError.

    py> func(**{a, b, c})
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: func() argument after ** must be a mapping, not set


> And of course existing linters, IDEs, etc. will misunderstand the new 
> syntax (which is worse than failing to parse it) until they’re taught 
> the new special case.

Do you have any examples?


> This also raises an implementation issue. The grammar rule to 
> disambiguate this will probably either be pretty hairy, or require 
> building a parallel fork of half the expression tree so you can have 
> an “expression except for set displays” node. Or there won’t be one, 
> and it’ll be done as a special case post-parse hack, which Python uses 
> sparingly.

Obviously if the implementation is hairy enough, that counts against the 
proposal. But given that there is no realistic chance of this going into 
Python 3.9 (feature freeze is not far away), and Python 3.10 will be 
using the new PEG parser, let's not rule it out *just* yet.


-- 
Steven
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/TYXNML2C7PRXXERGZL4AIEZGRNISVNRL/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to