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? * When the late-bound default is evaluated, what is the name resolution rule? (Which variables from which scopes will be seen?) These are fundamental details related to the meaning of code, not relatively minor details such as the timing of when a destructor will run. 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. 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. 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. 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. 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); * 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; * 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. 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. -- Steve