Re: PEP 671: Syntax for late-bound function argument defaults
While I love this idea, and I think this PEP is bound for glory, I don't love any of the proposed spellings for denoting late-binding arguments. It seems to me that a specialized symbol combination indicating that a particular argument has special behavior does not serve readability, regardless of what symbol combination is chosen. I would prefer to build on the fact that arguments already come in two flavors with somewhat different behaviors, and that the ordering of these is determined. Going by this analogy, it would make sense to have late-binding arguments following keyword arguments, set off by some separator, such as a double-pipe: def my_func(positional_arg, some_kwarg="foo" || late_binding_arg=[]]: # body of func or def my_func(positional_arg, some_kwarg="foo" || late_binding_arg=[]): # body of func I'm not massively attached to this particular symbol (which has advantages and disadvantages), but more to the idea that we can easily see that the arguments to the right of it are late-binding, and we can easily teach a beginner what that means. We can also easily scan a function definition and know that there is some specialized behavior going on, which is harder to see with a spelling like def my_func(positional_arg, some_kwarg<="foo", another_kwarg=[]): # body of func and we will probably even notice much more easily that there is a likely bug in that last definition (since we probably meant for the list arg to be late-binding) Best -Jon Kiparsky jon.kiparsky@gmail.com
On Mon, Oct 25, 2021 at 11:41 PM Jon Kiparsky <jon.kiparsky@gmail.com> wrote:
I would prefer to build on the fact that arguments already come in two flavors with somewhat different behaviors, and that the ordering of these is determined. Going by this analogy, it would make sense to have late-binding arguments following keyword arguments, set off by some separator, such as a double-pipe:
def my_func(positional_arg, some_kwarg="foo" || late_binding_arg=[]]: # body of func
or
def my_func(positional_arg, some_kwarg="foo" || late_binding_arg=[]): # body of func
Interesting, but what are the consequences at the call site? Bear in mind that default argument values can be applied to any argument, or not applied to any argument, and these have implications on what is optional and what is mandatory: def f(a, b=1, /, d=2, *, e, f=3): print(a, b, "(c)", d, e, f) def g(a, /, c, d=2, *, e, f=3): print(a, "(b)", c, d, e, f) You can't have a positional-only argument with a default, followed by a positional-or-keyword argument without, so I can't show all six options in a single function. But all six options are possible, and they have these implications: * a must be passed positionally, and omitting it will TypeError * b may be passed positionally or omitted * c may be passed positionally or by name, but must be provided * d may be passed either way or not at all * e must be passed by name, and omitting it will TypeError * f may be passed by name, or may be omitted (d is so accommodating - he's a nice friendly fellow, he doesn't judge.) Having a new category of function parameters would make these calls even more complicated. It also overemphasizes, in my opinion, the difference between ways that optional arguments are provided with their values. But I do find this suggestion interesting. ChrisA
On Sun, Oct 24, 2021 at 07:40:26PM -0400, Jon Kiparsky wrote:
I would prefer to build on the fact that arguments already come in two flavors with somewhat different behaviors,
Three. - Positional only. - Positional-or-keyword. - Keyword only. plus two somewhat special `*args` and `**kwargs` parameters that collect whatever is remaining.
and that the ordering of these is determined. Going by this analogy, it would make sense to have late-binding arguments following keyword arguments, set off by some separator, such as a double-pipe:
I want two functions, spam and eggs. Both of them take two positional- or-keyword parameters, "first" and "second", which the caller gives in that order: spam(first, second) eggs(first, second) (Or by keyword, but in the usual case they are given as positional arguments.) In the spam function, first takes a default value using early binding, and second takes a default value using late binding. In the spam function, the defaults are the other way around. Here is how we might declare the function signatures: def spam(first=default, @second=default) def eggs(@first=default, second=default) How would we declare the functions using your proposed syntax? Remember that the calling order is not up for negotiation: it is "first" first and "second" second. I get: def spam(first=default || second=default) def eggs(second=default || first=default) How does the interpreter map those signatures to the calling order? (Please don't say "alphabetical order" *wink*)
We can also easily scan a function definition and know that there is some specialized behavior going on, which is harder to see with a spelling like
def my_func(positional_arg, some_kwarg<="foo", another_kwarg=[]): # body of func
Aside from the symbol being a less-than-or-equal operator, that is a good argument against modifying the equals sign. It disappears into the middle of something which is likely to be quite busy: some_arg:Type=expression where both the Type and the expression are likely to be moderately complex chunks of text. (By the way, there is absolutely no good reason to make a string constant like "foo" a late-bound default. Of course we should allow it syntactically, a string literal is an expression, but linters and code-checkers should flag it.) If we want to make it obvious which parameters use late binding, and we should, then we should tag the parameter with a prefix, as in my examples above. -- Steve
participants (3)
-
Chris Angelico
-
Jon Kiparsky
-
Steven D'Aprano