On 2021-12-12 23:49, Steven D'Aprano wrote:
On Sun, Dec 12, 2021 at 12:07:30PM -0700, Carl Meyer wrote:
I don't think this is a fair dismissal of the concern. Taken broadly, function "wrapping" is extremely common, in the sense that what many (most?) functions do is call other functions, and there is a wide spectrum of "wrapping" from "pure wrapper that does not change the signature at all" through "non-trivial wrapper that has a different signature but requires some of the same arguments with the same semantics" all the way to "not a wrapper at all because it uses the called function for a very small portion of what it does and shares no signature with it."
In any case where the same argument with the same semantics needs to be passed through multiple layers of a function call chain (and again, my experience is this is quite common in real world code; I can collect some data on this if anyone finds this assertion unconvincing), non-trivial argument defaults are painful. One is faced with two unappealing options: either duplicate the non-trivial default (in which case you have code duplication and more places to update on any change), or give up entirely on introspectable/legible signatures and use `*args, **kwargs`.
I think you have identified a real pain point, function wrapping, but it is a much bigger pain point that just function defaults, and it won't be made appreciably worse by late-bound defaults.
Over the years, I have written about this pain point a number of times. It is much more significant than just defaults: it is hits parameter naming, and order, and annotations.
The problem is that we have no good notation to say that one function inherits its signature (possibly with changes) from another function.
This is especially noticable during rapid experimental development, where the signature of functions might be changing rapidly, but it occurs in stable code too. Even if the signature is changing, the wrapper nevertheless has to be aware of the signature, and duplicate it.
The idea that this problem is unique, or especially acute, for function defaults is false. It is a problem across the board:
- if the *order of parameters* changes, the wrapping function must also change, unless it exclusively uses keyword arguments;
- if the *parameter names* change, the wrapping function likewise must also change, unless it exclusively uses positional arguments;
- if the *parameter type declarations* change, the wrapping function should also change, lest the reader get confused, and static or runtime type checkers flag the code as wrong.
Or to put it more succinctly:
The wrapper must be aware of the wrapped signature in order to duplicate it. Function signatures are code, and this is a violation of DRY.
If anything, the "default argument" problem is the *least* issue here, not the most, because the wrapper can, at least sometimes, just omit the parameter with a default.
The only solution to this right now is to use `*args, **kwargs`.
If that hurts introspection, then that demonstrates a weakness in our introspection tools that needs to be fixed.
If you try to reduce the problem by removing defaults, or annotations, or only using keyword arguments, or only using positional arguments, not only does it not solve the problem, but the solution is worse than the problem being solved.
But if we focus only on one tiny little corner of the problem space, complex defaults (whether early or late), there is one other possible mitigation that is out of scope for this PEP but perhaps we could consider it. Namely a Javascript-like undefined value that the interpreter can use as an explicit signal to "treat this as a missing argument and use the default".
But undefined has its own problems to, and its not clear to me that this would be any more of a solution to the tight coupling between wrapper and wrapped functions problem than any of the other non-solutions are.
[snip] Hmm. What about something like this as a bit of syntax: def my_decorator(f): @wraps def wrapper(from my_decorator): return f(from my_decorator) return wrapper The idea is that in a function's parameter list it would pick up the signature and in a function call it would pick up the arguments.