
On Sun, Oct 31, 2021 at 9:54 AM Brendan Barnwell <brenbarn@brenbarn.net> wrote:
On 2021-10-30 15:35, Chris Angelico wrote:
Bear in mind that the status quo is, quite honestly, a form of white lie. In the example of bisect:
def bisect(a, hi=None): ... def bisect(a, hi=>len(a)): ...
neither form of the signature will actually say that the default value is the length of a. In fact, I have never said that the consumer should eval it. There is fundamentally no way to determine the true default value for hi without first figuring out what a is.
The way you use the term "default value" doesn't quite work for me. More and more I think that part of the issue here is that "hi=>len(a)" we aren't providing a default value at all. What we're providing is default *code*. To me a "value" is something that you can assign to a variable, and current (aka "early bound") argument defaults are that. But these new late-bound arguments aren't really default "values", they're code that is run under certain circumstances. If we want to make them values, we need to define "evaluate len(a) later" as some kind of first-class value.
This is correct. In fact, the way it is in the syntax, the second one is a "default expression". But the Signature object can't actually have the default expression, so it would have to have either a default value, or some sort of marker. Argument defaults, up to Python 3.11, are always default values. If PEP 671 is accepted, it will make sense to have default values AND default expressions.
So which is better: to have the value None, or to have a marker saying "this will be calculated later, and here's a human-readable description: len(a)"?
Let me say that in a different way. . . :-)
Which is better, to have a marker saying "this will be calculated later", or to have a marker saying "this will be calculated later, and here's a human-readable description"?
My point is that None is already a marker. It's true it's not a special-purpose marker meaning "this will be calculated later", but I think in practice it is a marker that says "be sure to read the documentation to understand what passing None will do here".
That's true. The trouble is that it isn't uniquely such a marker, and in fact is very often the actual default value. When you call dict.get(), the second argument has a default of None, and if you omit it, you really truly do get None as a result. Technically, for tools that look at func.__defaults__, I have Ellipsis doing that kind of job. But (a) that's far less common than None, and (b) there's a way to figure out whether it's a real default value or a marker. Of course, there will still be functions that have pseudo-defaults, so you can never be truly 100% sure, but at least you get an indication for those functions that actually use default expressions. So what you have is a marker saying "this is either the value None, or something that will be calculated later". Actually there are multiple markers; None might mean that, but so might "<object object at 0x7f27e77f0570>", which is more likely to mean that it'll be calculated later, but harder to recognize reliably. And in all cases, it might not be a value that's calculated later, but it might be a change in effect (maybe causing an exception to be raised rather than a value being returned). The markers currently are very ad-hoc and can't be depended on by tools. There is fundamentally no way to do better than a marker, but we can at least have more useful markers.
Increasingly it seems to me as if you are placing inordinate weight on the idea that the benefit of default arguments is providing a "human readable" description in the default help() and so on. And, to be frank, I just don't care about that. We can already provide human-readable descriptions in documentation and we should do that instead of trying to create gimped human-readable descriptions that only work in special cases. Or, to put it even more bluntly, from my perspective, having help() show something maybe sort of useful just in the case where the person wrote very simple default-argument logic and didn't take the time to write a real docstring is simply not a worthwhile goal.
Interesting. Then why have default arguments at all? What's the point of saying "if omitted, the default is zero" in a machine-readable way? After all, you could just have it in the docstring. And there are plenty of languages where that's the case. I'm of the opinion that having more information machine-readable is always better. Are you saying that it isn't? Or alternatively, that it's only useful when it fits in a strict subset of constant values (values that don't depend on anything else, and can be calculated at function definition time)?
So really the status quo is "you can already have the human-readable description but you have to type it in the docstring yourself". I don't see that as a big deal. So yes, the status quo is better, because it is not really any worse, and it avoids the complications that are arising in this thread (i.e., what order are the arguments evaluated in, can they reference each other, what symbol do we use, how do we implement it without affecting existing introspection, etc.).
And it also allows an easy transformation for functions that currently are experiencing issues from things being too constant. Consider: default_timeout = 500 def connect(timeout=default_timeout): If that default can be changed at run time, how do you fix the function? By my proposal, you just mark the default as being late-bound. With the status quo, now you need to bury the real default in the body of the function, and make a public statement that the default is None (or something else). That's not a true statement, since None doesn't really make sense as a default, but that's what you have to do to work around a technical limitation. ChrisA