
On Tue, Oct 26, 2021 at 3:00 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Tue, Oct 26, 2021 at 04:48:17AM +1100, Chris Angelico wrote:
The problem is the bizarre inconsistencies that can come up, which are difficult to explain unless you know exactly how everything is implemented internally. What exactly is the difference between these, and why should some be legal and others not?
They should all be legal. Legal doesn't mean "works". Code that raises an exception is still legal code.
Then there's no such thing as illegal code, and my entire basis for explanation is bunk. Come on, you know what I mean. If it causes SyntaxError:, it's not legal code. Just because that's a catchable exception doesn't change anything. Example:
def f5(x=>y + 1): global y y = 2
According to the previously-defined equivalencies, this would mean: def f5(x=None): if x is None: x = y + 1 global y y = 2 And that's a SyntaxError. Do you see what I mean now? Either these things are not consistent with existing idioms, or they're not consistent with each other. Since writing that previous post, I have come to the view that "consistency with existing idioms" is the one that gets sacrificed to resolve this. I haven't yet gotten started on implementation (definitely gonna get to that Real Soon Now™), but one possible interpretation of f5, once disconnected from the None parallel, is that omitting x would use one more than the module-level y. That implies that a global statement *anywhere* in a function will also apply to the function header, despite it not otherwise being legal to refer to a name earlier in the function than the global statement.
And lastly, f5() assigns positional arguments first (there are none), then keyword arguments (still none), then early-bound defaults left to right (none of these either), then late-bound defaults left to right (x=y+1) which might raise NameError if global y doesn't exist, otherwise it will succeed.
It's interesting that you assume this. By any definition, the header is a reference prior to the global statement, which means the global statement would have to be hoisted. I think that's probably the correct behaviour, but it is a distinct change from the current situation.
However there is a real, and necessary, difference in behaviour which I think you missed:
def func(x=x, y=>x) # or func(x=x, @y=x)
The x=x parameter uses global x as the default. The y=x parameter uses the local x as the default. We can live with that difference. We *need* that difference in behaviour, otherwise these examples won't work:
def method(self, x=>self.attr) # @x=self.attr
def bisect(a, x, lo=0, hi=>len(a)) # @hi=len(a)
Without that difference in behaviour, probably fifty or eighty percent of the use-cases are lost. (And the ones that remain are mostly trivial ones of the form arg=[].) So we need this genuine inconsistency.
I agree, we do need that particular inconsistency. I want to avoid others where possible.
If you can live with that actual inconsistency, why are you losing sleep over behaviour (functions f1 through f4) which isn't actually inconsistent?
(Sleep? What is sleep? I don't lose what I don't have!) 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): ... in that it changes what's viable and what's not. (Since you don't like the term "legal" here, I'll go with "viable", since a runtime exception isn't terribly useful.) Changing the default from y=2 to y=>2 would actually stop the example from working. Multi-pass initialization makes sense where it's necessary. Is it really necessary here?
And importantly, do Python core devs agree with less-skilled Python programmers on the intuitions?
We should write a list of the things that Python wouldn't have if the intuitions of "less-skilled Python programmers" was a neccessary condition.
- no metaclasses, descriptors or decorators; - no classes, inheritence (multiple or single); - no slices or zero-based indexing; - no mutable objects; - no immutable objects; - no floats or Unicode strings;
etc. I think that, *maybe*, we could have `print("Hello world")`, so long as the programmer's intuition is that print needs parentheses.
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.
Two-phase initialization is my second-best preference after rejecting with SyntaxError, but I would love to see some real-world usage before opening it up. Once permission is granted, it cannot be revoked, and it might turn out that one of the other behaviours would have made more sense.
Being cautious about new syntax is often worthy, but here you are being overcautious. You are trying to prohibit something as a syntax error because it *might* fail at runtime. We don't even protect against things that we know *will* fail!
x = 1 + 'a' # Not a syntax error.
But this is an error: x = 1 def f(): print(x) x = 2 And so is this: def f(x): global x As is this: def f(): x = 1 global x x = 2 You could easily give these functions meaning using any of a variety of rules, like "the global statement applies to what's after it" or "the global statement applies to the whole function regardless of placement". Why are they SyntaxErrors? Is that being overcautious, or is it blocking code that makes no sense? The two-pass model is closer to existing idioms. That's of value, but it isn't the greatest justification. And given that there is no idiom that perfectly matches the semantics, I don't consider that to be strong enough to justify the increase in complexity. ChrisA