
On Tue, Oct 26, 2021 at 11:10 PM Steven D'Aprano <steve@pearwood.info> wrote:
Based on the multi-pass assignment model, which you still favour, those WOULD be quite inconsistent, and some of them would make little sense. It would also mean that there is a distinct semantic difference between:
def f1(x=>y + 1, y=2): ... def f2(x=>y + 1, y=>2): ...
Sure. They behave differently because they are different.
These are different too:
# Block 1 y = 2 x = y + 1
# Block 2 x = y + 1 y = 2
Yes, those ARE different. Those are more equivalent to changing the order of the parameters in the function signature, and I think we all agree that that DOES make a difference. The question is whether these could change meaning if you used a different type of assignment, such as: y := 2 x = y + 1 Does that suddenly make it legal? I think you'll find that this sort of thing is rather surprising. And that's what we have here: changing from one form of argument default to another changes whether left-to-right applies or not. I don't want that. And based on an experiment with a less-experienced Python programmer (admittedly only a single data point), neither do other people. Left-to-right makes sense; multi-pass does not.
Multi-pass initialization makes sense where it's necessary. Is it really necessary here?
We already have multi-pass initialisation.
1. positional arguments are applied, left to right; 2. then keyword arguments; 3. then defaults are applied.
(It is, I think, an implementation detail whether 2 and 3 are literally two separate passes or whether they can be rolled into a single pass. There are probably many good ways to actually implement binding of arguments to parameters. But semantically, argument binding to parameters behaves as if it were multiple passes.
Those aren't really multi-pass assignment though, because they could just as easily be assigned simultaneously. You can't, in Python code, determine which order the parameters were assigned. There are rules about how to map positional and keyword arguments to the names, but it would be just as logical to say: 1. Assign all defaults 2. Assign all keyword args, overwriting defaults 3. Assign positional args, overwriting defaults but not kwargs And the net result would be exactly the same. But with anything that executes arbitrary Python code, it matters, and it matters what state the other values are in. So we have a few options: a) Assign all early-evaluated defaults and explicitly-passed arguments, leaving others unbound; then process late-evaluated defaults one by one b) Assign parameters one by one, left to right
Since the number of parameters is likely to be small (more likely 6 parameters than 6000), we shouldn't care about the cost of a second pass to fill in the late-bound defaults after all the early-bound defaults are done.
I'm not concerned with performance, I'm concerned with semantics.
No, you misunderstand. I am not saying that less-skilled programmers have to intuit things perfectly; I am saying that, when there are drastic differences of expectation, there is probably a problem.
I can easily explain "arguments are assigned left to right". It is much harder to explain multi-stage initialization and why different things can be referenced.
I disagree that it is much harder.
In any case, my fundamental model here is that if we can do something using pseudo-late binding (the "if arg is None" idiom), then it should (more or less) be possible using late-binding.
We should be able to just move the expression from the body of the function to the parameter and in most cases it should work.
There are enough exceptions that this parallel won't really work, so I'd rather leave aside the parallel and just describe how argument defaults work. Yes, you can achieve the same effect in other ways, but you can't do a mechanical transformation and expect it to behave identically.
Inside the body of a function, we can apply pseudo-late binding using the None idiom in any order we like. As late-binding parameters, we are limited to left-to-right. But we can get close to the (existing) status quo by ensuring that all early-bound defaults are applied before we start the late-bound defaults.
# Status quo def function(arg, spam=None, eggs="something useful"): if spam is None: spam = process(eggs)
eggs is guaranteed to have a result here because the early-bound defaults are all assigned before the body of the function is entered. So in the new regime of late-binding, I want to write:
def function(arg, @spam=process(eggs), eggs="something useful"):
and the call to process(eggs) should occur after the early bound default is assigned. The easiest way to get that is to say that early bound defaults are assigned in one pass, and late bound in a second pass.
Without that, many use cases for late-binding (I won't try to guess a proportion) are not going to translate to the new idiom.
Can you find some actual real-world cases where this is true? I was unable to find any examples where I didn't have to apologize for the contrivedness of them. Having an argument default depend on arguments that come after it seems very surprising, especially since they can't be passed positionally anyway; so it would only be a very narrow set of circumstances where this is a problem - if they're keyword-only args, they can be reordered into something more logical, thus solving the problem. I think you're far too caught up on equivalences that don't exist. ChrisA