On Fri, Oct 29, 2021 at 8:11 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Fri, Oct 29, 2021 at 07:17:05PM +1100, Chris Angelico wrote:
* Argument defaults (either in __defaults__ or __kwdefaults__) are now tuples of (desc, value) or (desc,) for early-bound and late-bound respectively * Early-bound defaults get mapped as normal. Late-bound defaults are left unbound at time of function call.
Pardon me if this has already been discussed, but wouldn't it be better to leave defaults and kwdefaults alone, and add a new pair of attributes for late bound defaults? `__late_defaults__` and `__late_kwdefaults__`.
The trouble with that is that positional arguments could have any combination of early and late defaults. If the late ones are in a separate attribute, there'd need to be some sort of synchronization between them. (It would work for kwdefaults, but they only apply to kwonly args - positional-or-keyword args go into __defaults__.)
Otherwise its a backwards-incompatable change to the internals of the function object, and one which is not (so far as I can tell) necessary.
Obviously you need a way to indicate that a value in __defaults__ should be skipped. Here's just a sketch. Given:
def func(a='alpha', b='beta', @c=expression, d=None)
where only c is late bound, you could have:
__defaults__ = ('alpha', 'beta', None, None) __late_defaults__ = (None, None, <code for expression>, None)
The None values in __defaults__ mean to look in the __late_defaults__ tuple. If the appropriate value there is also None, return it, otherwise the parameter is late-bound. Evaluate it and return the result.
Except that that's still backward-incompatible, since None is a very common value. So this form of synchronization wouldn't work; in fact, *by definition*, any object can be in __defaults__, so there's no possible sentinel that can indicate that late defaults should be checked. The only way would be to first look in late defaults, and only then look in defaults... which is basically the same as I have, only all in a single attribute.
That means that param=None will be a little bit more costly to fill at function call time than it is now, but not by much. And non-None defaults won't have any significant extra cost (just one check to see if they are None).
And if you really want to keep arg=None as fast as possible, we could use some other sentinel like NotImplemented that is much less common.
Having "a bit less" backward incompatibility isn't really a solution; if we need to have any, why have all the complexity?
So far unimplemented is the description of the argument default. My plan is for early-bound defaults to have None there (as they currently do), but late-bound ones get the source code.
That's not what I see currently in 3.10:
>>> def func(a=1, b=2, c="hello"): ... pass ... >>> func.__defaults__ (1, 2, 'hello')
What am I missing?
Currently, you don't get a description, you get a value.
def func(a=0x10, b=16, c=0o20): pass ... func.__defaults__ (16, 16, 16)
That's not a big deal with early-bound, but it's kinda crucial with late-bound, since the description is the only thing you'd get. The spec I'm currently going for is that a description of None means "use the repr of the value", and that's what early-bound defaults will use, so that has the same behaviour as current 3.11, and is the way it's currently implemented in the PEP 671 branch. What needs to change is late-bound defaults. Anyway, if someone wants to make changes to the implementation, or even do up their own from scratch, I would welcome it. It's much easier to poke holes in an implementation than to actually write one that is perfect. And fundamentally, there WILL be behavioural changes here, so I'm not hugely bothered by the fact that the inspect module needs to change for this. ChrisA