On Fri, 17 Jun 2022 at 15:55, Chris Angelico <ros...@gmail.com> wrote:
>
> On Sat, 18 Jun 2022 at 00:21, Paul Moore <p.f.mo...@gmail.com> wrote:
> >
> > On Fri, 17 Jun 2022 at 14:15, Chris Angelico <ros...@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
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/SES7PZRP3VPZF5CIQR56G46KGDPYYAB4/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to