
On Fri, 17 Jun 2022 at 22:55, Andrew Jaffe <a.h.jaffe@gmail.com> wrote:
First, let me state that I am in favour of the proposal (although still mildle prefer the ":=" spelling).
On 17/06/2022 13:33, Chris Angelico wrote:
On Fri, 17 Jun 2022 at 22:14, Steven D'Aprano <steve@pearwood.info> wrote:
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.
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.
I understand this is unambiguous, but it is nonetheless potentially confusing: normal, immediate-evaluation arguments do, of course, have access to enclosing scope, and so one might be led to believe that this is still possible.
That's because immediate-evaluation is like this: _default = SOME_EXPRESSION def func(n=None): if n is None: n = _default Whereas late evaluation is like this: def func(n=None): if n is None: n = SOME_EXPRESSION Unfortunately, there's not going to be any way to resolve this. The entire point of this feature is to be able to do things that can't be done with early evaluation, and that includes referring to other arguments, so it fundamentally has to be done in the function's scope. Maybe it would have been convenient for Python to define that function defaults are *always* evaluated in the function's scope, but (a) that ship has well and truly sailed, and (b) I'm not sure that that would be better anyway - the current behaviour lets you do an easy snapshot by writing something like "i=i", so it has the same name on the inside that it has on the outside. For the most part, it's not a problem; scopes are nested, so you can happily refer to a name in an enclosing scope. Exceptions include names that are shadowed, class-level names (but you'll usually be able to write "self.X" or "cls.X" so it's just a difference of spelling), and possibly some quirks of closures, although most of those are a consequence of timing rather than scoping.
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.
Is there a *reason* why you are leaving this unspecified? To put it more baldly, is there any reason (e.g., difficulty of parsing?) why allowing these "forward" references should *not* be allowed? It seems that "n=>len(items), items=[]" might be an important use case.
Yes. The alternative is that I make it "reference-implementation-defined", and I've seen so much of that that I don't want to lock that in. Just because the way I happen to have implemented it allows for the late-bound defaults to refer to early-bound arguments to their right, I don't want to lock the language into behaving that way forever; conversely, I don't want to have a non-compliant reference implementation based on a definition of "all arguments are assigned left to right", which is much cleaner and simpler to describe, but a lot harder to implement. I want the language to be open to the cleaner definition, while permitting the "arguments are assigned left to right in two stages" implementation as well. That's the only distinction though. And if you simply place all late-bound defaults to the right of all early-bound defaults, there won't be any problem, ever. ChrisA