On Wed, 1 Dec 2021 at 06:19, Chris Angelico <rosuav@gmail.com> wrote:
I've just updated PEP 671 https://www.python.org/dev/peps/pep-0671/ with some additional information about the reference implementation, and some clarifications elsewhere.
*PEP 671: Syntax for late-bound function argument defaults*
Questions, for you all:
1) If this feature existed in Python 3.11 exactly as described, would you use it?
Most likely. ---
2) Independently: Is the syntactic distinction between "=" and "=>" a cognitive burden?
Presented in isolation, like that, no — however I do feel that the distinguishing character is the at the wrong side of the equals. Default values may start with a prefix operator (`+`, `-`, `~`), thus it could be possible to incorrectly interpret the `>` as some sort of quote/defer prefix operator (or just difficult to spot) when additional whitespace is lacking. In other words, I think these look a little too similar: def func(arg=-default): ... def func(arg=>default): ... Additionally `=>` would conflict with the proposed alternate lambda syntax, both cognitively and syntactically — assuming the `=>` form would be valid everywhere that a lambda expression is currently (without requiring additional enclosing parentheses). The following is legal syntax: def func(arg: lambda x: x = 42): ... # for clarification: # func.__defaults__ == (42,) # func.__annotations__ == {'arg': <function <lambda> at 0x...>} It doesn't look promising to place the marker for late bound defaults on other side of the equals either — causing a syntactical conflict with comparison operators or assignment operator (or cognitive conflict augmented assignment) depending on the choice of character. This leads me to favour the `@param=default` style and although I agree with Abe Dillon that this somewhat mimics the `*args` and `**kwds` syntax, I don't see this parallel as a negative. We already have some variation of late binding in parameter lists, where? `*args` and `**kwds`: both are rebound upon each call of the function. Another odd (though not useful) similarity with the current proposal is that function objects also lack attributes containing some kind of special representation of the `*args` and `**kwds` parameter defaults (i.e. the empty tuple & dict). One **cannot** successfully perform something akin to the following: def func(**kwds): return kwds func.__kwds_dict_default__ = {'keyword_one': 1} assert func() == {'keyword_one': 1} Just as with the proposal one cannot modify the method(s) of calculation used to obtain the late bound default(s) once a function is defined. I don't know that I have a strong preference for the specific marker character, but I quite like how `@param=default` could be understood as "at each (call) `param` defaults to `default`". ---
3) If "yes" to question 1, would you use it for any/all of (a) mutable defaults, (b) referencing things that might have changed, (c) referencing other arguments, (d) something else?
Likely all three, maybe all four. A combination of (b) & (c) could be particularly useful with methods since one of those other arguments is `self`, for example: class IO: def truncate(self, position=>self.tell()): ... ---
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. 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 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`). 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. 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.