On Sat, Dec 4, 2021 at 6:33 AM Adam Johnson <mail.yogi...@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 _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/N7OASNASPIB6EMD65V7SZJIOPL4ULDRB/ Code of Conduct: http://python.org/psf/codeofconduct/