
On Fri, 17 Jun 2022 at 22:14, Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Jun 16, 2022 at 08:31:19AM +1000, Chris Angelico wrote:
On Thu, 16 Jun 2022 at 08:25, Steven D'Aprano <steve@pearwood.info> wrote:
Under the Specification section, the PEP explicitly refers to behaviour which "may fail, may succeed", and different behaviour which is "Highly likely to give an error", and states "Using names of later arguments should not be relied upon, and while this MAY work in some Python implementations, it should be considered dubious".
So, yes, the PEP *punts* on the semantics of the feature, explicitly leaving the specification implementation-dependent.
One very very specific aspect of it is left undefined. Are you really bothered by that?
Yes.
This is not just some minor, trivial implementation issue, it cuts right to the core of this feature's semantics:
* Which arguments can a late-bound parameter access?
Definitely those that come before it. Potentially some of those that come after it, but I'm not mandating that.
* When the late-bound default is evaluated, what is the name resolution rule? (Which variables from which scopes will be seen?)
Exactly the same as any other code executed in the function. I really don't see why this is so surprising to you; it is *exactly the normal behaviour of Python code*.
These are fundamental details related to the meaning of code, not relatively minor details such as the timing of when a destructor will run.
They're exactly as fundamental, actually. The 'with' statement was added to the language specifically because it was critically important to clean things up in ways that CPython does, but Python does not guarantee. In my reference implementation, more is available than the PEP guarantees. That's all.
If we have:
``` items = ['spam', 'eggs'] def frob(n=>len(items), items=[]): print(n)
```
we cannot even tell whether `frob()` will print 0 or 2 or raise an exception.
It will either print 0 or raise UnboundLocalError. There is no circumstance in which it will legally print 2.
I described this underspecification as a weakness of the PEP. As I said at the time, that was my opinion. As the PEP author, of course it is your perogative to leave the semantics of this feature underspecified, hoping that the Steering Council will be happy with implementation- dependent semantics.
It's a lot less implementation-dependent than you seem to think.
For the benefit of other people reading this, in case it isn't clear, let me try to explain what the issue is.
When late-bound defaults are simulated with the `is None` trick, we write:
``` def frob(n=None, items=[]): # If we enter the body of the function, # items is guaranteed to have a value. if n is None: n = len(items) print(n) ```
and there is never any doubt about the scoping rules for `len(items)`. It always refers to the parameter `items`, never to the variable in the surrounding scope, and because that parameter is guaranteed to be bound to a value, so the simulated default `len(items)` cannot fail with NameError. We can reason about the code's meaning very easily.
This is exactly the same. I don't understand what's confusing here.
If we want "real" late-bound defaults to match that behaviour, `n=>len(items)` must evaluate `len(items)` *after* items is bound to a value, even though items occurs to the right of n.
Yes, and the ONLY part that is underspecified is the order. It is perfectly legal for it to be implemented in either of these ways: def frob(): n = len(items) items = [] def frob(): items = [] n = len(items) And I would consider code that depends on specifically one or the other to be bad code, just like code that depends on __del__ methods being called. What wording could I add to the PEP to make this more obvious?
Under the PEP though, this behaviour is underspecified. The PEP describes this case as implementation dependent. Any of the following behaviours would be legal when `frob()` is called:
* n=>len(items) evaluates the parameter `items`, *after* it gets bound to the default of [], and so n=0 (that is, it has the same semantics as the status quo);
Yes, this is legal.
* n=>len(items) evaluates the parameter `items`, but it isn't bound to a value yet (because `items` occurs to the right of n), and so evaluating the default raises (presumably) UnboundLocalError;
Yes, this is legal.
* n=>len(items) evaluates the variable items from the surrounding scope, and so evaluates to n=2; if no such variable exists, it will presumably raise NameError.
No, this makes no sense whatsoever. In Python, a parameter is (effectively) assigned to within the function, and therefore *any* reference to it *must* refer to the local, not to any surrounding scope. Late-bound defaults do not change this fundamental.
With the behaviour unspecified, we can't predict whether the above frob() example is legal or what it will do if it is. It could vary not only between CPython and other Pythons, but from one version of CPython and another.
That is correct. This issue ONLY happens if a late-bound default refers to an early-bound argument that comes to the right of it in the argument list, and the ONLY possible results are UnboundLocalError and getting the value. Please, provide some better wording for the PEP, something that would have convinced you that this is the case. I am getting very tired of you not reading my posts, and continuing to argue from ignorance. ChrisA