On Fri, 17 Jun 2022 at 15:55, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, 18 Jun 2022 at 00:21, Paul Moore <p.f.moore@gmail.com> wrote:
On Fri, 17 Jun 2022 at 14:15, Chris Angelico <rosuav@gmail.com> wrote:
There are several ways to make this clearly sane.
# Clearly UnboundLocalError def frob(n=>len(items), items=>[]):
Um, I didn't see that as any more obvious than the original example. I guess I can see it's UnboundLocalError, but honestly that's not obvious to me.
Question: Is this obvious?
def f(): x, x[0] = [2], 3 print(x)
def boom(): x[0], x = 3, [2] # raises UnboundLocalError
No. I'm not sure what point you're trying to make here?
I understand that left-to-right evaluation is something that has to be learned (and isn't 100% true - operator precedence is a thing too), but at very least, if it isn't *obvious*, it should at least be *unsurprising* if you then get UnboundLocalError.
Why? Are you saying I can't be surprised by the details of rules that I don't often have a need to understand in detail? I fear we're getting off-topic here, though. I'm not arguing that anything here isn't well-defined, just that it's not obvious *to me*. And I'm not even "arguing" that, I'm simply stating it as an observed fact about how I initially reacted to the quoted example. It's you who is stating that the frob case is "clearly" UnboundLocalError, and all I'm saying is that's not "clear" to me, even if it is a consequence of the rules in the PEP. And actually, I could argue that the PEP would benefit from some clarification to make that consequence clearer - but I don't feel that you're likely to be particularly receptive to that statement. In case you are, consider that as written, the PEP says that the *defaults* are evaluated left to right in the function's runtime scope, but it doesn't say when the parameter names are introduced in that scope - prior to this PEP there was no need to define that detail, as nothing could happen before the names were introduced at the start of the scope. If you accept that clarification, can you accept that the current text isn't as clear as it might be?
# Clearly correct behaviour def frob(items=[], n=>len(items)): def frob(items=>[], n=>len(items)):
Maybe... I'm not sure I see this as *that* much more obvious, although I concede that the left-to-right evaluation rule implies it (it feels like a mathematician's use of "obvious" - which quite often isn't ;-)) Using assignment expressions in argument defaults is well-defined but not necessarily obvious in a similar way (to me, at least).
When you say "assignment expressions", do you mean "default expressions", or are you referring to the walrus operator? There's a lot of other potentially-surprising behaviour if you mix assignment expressions in with this, because of the difference of scope. It's the sort of thing that can definitely be figured out, but I would advise against it.
I meant the walrus operator, and that's my point. There's a lot of not-immediately-obvious interactions here. Even if we don't include default expressions, I'd argue that the behaviour is non-obvious:
def f(a=(b:=12)): ... print(a, b) ... f() 12 12 b 12
I assume (possibly naïvely) that this is defined in the language spec, though, as it's existing behaviour. But when you add in default expressions, you need to be sure that the various interactions are well-defined. Note that at this point, I'm not even talking about "obvious", simply the bare minimum of "if I write this supposedly legal code, does the PEP explain what it does?"
def frob(items=>[], n=>len(items:=[])):
This will reassign items to be an empty list if n is omitted. Obviously that's bad code, but in general, I think assignment expressions inside default expressions are likely to be very surprising :)
Agreed. Although consider the following:
def f(a=(b:=12), b=9): ... print(a, b) ... f() 12 9 b 12
Would def frob(n=>len(items:=[]), items=>[1,2]): ... reassign items if n is omitted? Or would it assign the *global* items and then shadow it with a local for the parameter? Can you point to the explanation in the PEP that covers this? And even if you can, are you trying to claim that the behaviour is "obvious"?
Then let's leave aside the term "obvious" and just go for "unsurprising". If you write code and get UnboundLocalError, will you be surprised that it doesn't work? If you write code and it works, will you be surprised with the result you got?
As I noted above, "surprising" is no different. I can easily be surprised by well-defined behaviour. I'm not arguing that there's no explanation for why a particular construct works the way that it does, just that the behaviour may not be intuitive to people even if it is a consequence of the rules. I'm arguing that the behaviour fails an "is this easy to teach" criterion, not "is this logically consistent".
Once you learn the basic idea of left-to-right evaluation, it should be possible to try things out and get unsurprising results. That's what I'm hoping for.
Get "explainable" results, yes. But I thought Python was supposed to aspire to more than that, and match how people thought about things. "Executable pseudocode" and all that.
Feel free to state that there's not *enough* cases of people being confused by the semantics to outweigh the benefits, but it feels to me that there are a few people claiming confusion here, and simply saying "you shouldn't be confused, it's obvious" isn't really addressing the point.
Part of the problem is that one person seems to think that Python will completely change its behaviour, and he's spreading misinformation. Ignore him, look just at the proposal itself, and tell me if it's still confusing.
OK, if this is going to boil down to you asserting that the only problems here are with "one person" then I don't think it's worth continuing. I am not simply parroting "misinformation spread by that one person" (and you've made it very obvious already who that individual is, so please try to keep your personal problem with them out of your discussions with me). If you're not willing to accept my comments as feedback given in my own right, then it's you who is shutting down discussion here, and I don't see much point in trying to provide a good-faith response to you.
The only two possible behaviours are:
1) It does the single obvious thing: n defaults to the length of items, and items defaults to an empty tuple. 2) It raises UnboundLocalError if you omit n.
So why not pick one?
To be quite honest, I can't think of any non-toy examples where the defaults would be defined backwards, like this.
If that's the case, then what is the downside of picking one? Personally, I have a nagging feeling that I could find a non-toy example, but it's not that important to me. What I'm arguing is that there's no point in not picking a behaviour. You're saying you don't want to lock other implementations into the particular behaviour you choose - but you also don't have an example of where that would be a problem, so we're *both* arguing hypotheticals here.
It's not like Steven's constant panic-fear that "undefined behaviour" literally means the Python interpreter could choose to melt down your computer.
Oh, please. If that's the only way in which you can imagine implementation-defined behaviour being an issue, then you've lived a pretty sheltered life. How about "My code works on Python 3.12 but not on 3.13, because the behaviour in this case changed with no warning"? Sure, the PEP (and presumably the docs) said "don't do that", but you said above that people experiment and work out behaviour from those experiments. So breaking their code because they did precisely that seems at best pretty harsh.
There are *two* options, no more, no less, for what is legal.
Nope, there are two that you consider acceptable behaviour. And I don't disagree with you. But what's so magical about two? Why not have just one that's legal. Because people might disagree with your choice? You're the PEP author, let them. Or are you worried that this single point could cause the PEP to fail?
You're not *just* recommending this for style guides, you're also explicitly stating that you refuse to assign semantics to it.
It's unfair to say that I "refuse to assign semantics" as if I'm permitting literally any behaviour.
Don't put words into my mouth. You have stated that you won't require a particular behaviour. That's refusing to assign semantics. If it makes you feel better I'll concede that you're not allowing *arbitrary* semantics. By the way, a lot of this debate could be solved incredibly easily by writing the PEP in terms of code equivalence: def fn(p1=>e1, p2=>e2, p3=e3): body behaves the same as def fn(p1=(_d1:=object()), p2=(_d2:=object()), p3=e3): if p1 is _d1: p1 = e1 if p2 is _d2: p2 = e2 There's probably some details to flesh out, but that's precise and well-defined. Debates over whether the resulting behaviour is "obvious" or "intuitive" can then take place against a background where everyone agrees what will happen (and can experiment with real code to see if they are comfortable with it).
All I'm doing is saying that the UnboundLocalError is optional, *at this stage*. There have been far less-defined semantics that have remained in the language for a long time, or cases where something has changed in behaviour over time despite not being explicitly stated as implementation-defined. Is this legal?
def f(): x = 1 global x
Does Python mandate whether this is legal or not? If so, how far back in Python's history has it been defined?
*Shrug*. There was never a PEP about it, I suspect, and the behaviour was probably defined a long time before Python was the most popular language in the world. It would be nice if we still had the freedom that we did back then. Sadly, we don't. Maybe some people are *too* cautious nowadays. It's entirely possible I'm one of them. That's why we have the SC - if you're confident that your proposal is solid in spite of people like me complaining about edge cases, then submit it. I'll trust the SC's judgement.
The semantics, if this code is legal, are obvious: the name x must always refer to the global, including in the assignment above it. If it's not legal, you get an exception, not an interpreter crash, not your hard drive getting wiped, and not a massive electric shock to the programmer.
Sigh. You have a very narrow view of "obvious". I can think of other equally "obvious" interpretations. I won't list them because you'll just accuse me of being contrary. But I will say that I tried that code and you get an exception. But interestingly, it's a *syntax* error (name assigned before global declaration), not a *runtime* exception. I genuinely don't know which you intended to suggest would be the obvious behaviour...
Would you prefer that I simply mandate that it be permitted, and then a future version of Python changes it to be an exception? Or the other way around? Because I could do that. Maybe it would reduce the arguments. Pun intended, and I am not apologizing for it.
lol, I'm always up for a good pun :-) Are you still talking about the global example? Because I'd prefer you left that part of the language alone. And if you're talking about PEP 671, you know my answer (I'd prefer you permit it and define what it does, so it can't change in future). Paul