On Sat, Dec 4, 2021 at 6:33 AM Adam Johnson <mail.yogi841@gmail.com> wrote:
5) Do you know how to compile CPython from source, and would you be willing to try this out? Please? :)
I have.
The first unwelcome surprise was:
>>> def func(a=>[]): ... return a ...
>>> import inspect >>> inspect.signature(func).parameters['a'].default Ellipsis
Here the current behaviour of returning `Ellipsis` is very unfortunate, and I think could lead to a lot of head scratching — people wondering why they are getting ellipses in their code, seemingly from nowhere. Sure, it can be noted in the official documentation that `Ellipsis` is used as the indicator of late bound defaults, but third-party resources which aim to explain the uses of `Ellipsis` would (with their current content) leave someone clueless.
Yes. Unfortunately, since there is fundamentally no object that can be valid here, this kind of thing WILL happen. So when you see Ellipsis in a default, you have to do one more check to figure out whether it's a late-bound default, or an actual early-bound Ellipsis:
def func(a=..., b=>[]): pass ... sig = inspect.signature(func) sig.parameters["a"].default, sig.parameters["b"].default (Ellipsis, Ellipsis) sig.parameters["a"].extra, sig.parameters["b"].extra (None, '[]')
Ellipsis is less likely as a default than, say, None, so this will come up fairly rarely. When it does, anything that's unaware of late-bound defaults will see Ellipsis, and everything else will do a second lookup. (I could have the default show the extra instead, but that would lead to other confusing behaviour.)
Additionally I don't think it's too unreasonable an expectation that, for a function with no required parameters, either of the following (or something similar) should be equivalent to calling `func()`:
pos_only_args, kwds = [], {} for name, param in inspect.signature(func).parameters.items(): if param.default is param.empty: continue elif param.kind is param.POSITIONAL_ONLY: pos_only_args.append(param.default) else: kwds[name] = param.default
func(*pos_only_args, **kwds)
# or, by direct access to the dunders
func(*func.__defaults__, **func.__kwdefaults__)
The problem is that then, parameters with late-bound defaults would look like mandatory parameters. The solution is another check after seeing if the default is empty: if param.default is ... and param.extra: continue
The presence of the above if statement's first branch (which was technically unnecessary, since we established for the purpose of this example all arguments of `func` are optional / have non-empty defaults) hints that perhaps `inspect.Parameter` should grow another sentinel attribute similar to `Parameter.empty` — perhaps `Parameter.late_bound` — to be set as the `default` attribute of applicable `Parameter` instances (if not also to be used as the sentinel in `__defaults__` & `__kwdefaults__`, instead of `Ellipsis`).
Ah, I guess you didn't see .extra then. Currently the only possible meanings for extra are None and a string, and neither has meaning unless the default is Ellipsis; it's possible that, in the future, other alternate defaults will be implemented, which is why I didn't call it "late_bound". But it has the same functionality.
Even if the above were implemented, then only way to indicate that the late bound default should be used would still be by omission of that argument. Thus, if we combine a late bound default with positional-only arguments e.g.:
def func(a=>[], b=0, /): ...
It then becomes impossible to programmatically use the given late bound default for `a` whilst passing a value for `b`. Sure, in this simplistic case one can manually pass an empty list, but in general — for the same reasons that it could be "impossible" to evaluate a late bound default from another context — it would be impossible to manually compute a replacement value exactly equivalent to the default.
That's already the case. How would you call this function with a value for b and no value for a? When you make positional-only arguments, you are expecting that they will be passed from left to right. That's just how parameters work. I don't consider this to be a problem in practice.
Honestly the circumstances where one may wish to define a function such as that above seem limited — but it'd be a shame if reverting to use of a sentinel were required, just in order to have a guaranteed way of forcing the default behaviour.
If you actually need to be able to specify b without specifying a, then there are several options: 1) Use a sentinel. If it's part of your API, then it's not a hack. You might want to use something like None, or maybe a sentinel string like "new", but it's hard to judge with toy examples; in realistic examples, there's often a good choice. 2) Allow keyword arguments. That's exactly what they're for: to allow you to specify some arguments out of order. 3) Redefine the function so the first argument is list_or_count, such that func(0) is interpreted as omitting a and passing b. This is usually a messy API, but there are a few functions where it works (eg range(), and things that work similarly eg random.randrange). Personally, I'd be inclined to option 2, but it depends a lot on the API you're building. ChrisA