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/

Reply via email to