On 12/2/2021 6:36 PM, Chris Angelico wrote:
On Fri, Dec 3, 2021 at 7:54 AM Eric V. Smith <eric@trueblade.com> wrote:
Say I have a function with an early-bound default. I can inspect it and I can change it. One reason to inspect it is so that I can call the function with its default values. This is a form of wrapping the function. I realize "just don't pass that argument when you call the function" will be the response, but I think in good faith you'd have to admit this is more difficult than just passing some default value to a function call.
1) I want to call this function 2) I may want to not pass this argument 3) Ah, perfect! I will pass this argument with a value of somemod._SENTINEL.
Or alternatively:
1) I want to call this function. 2) Prepare a dictionary of arguments. Leave out what I don't want. 3) If I want to pass this argument, add it to the dictionary.
This way doesn't require reaching into the function's private information to use a sentinel. Yes, it may be a tad more difficult (though not VERY much), but you're also avoiding binding yourself to what might be an implementation detail.
Your version is less friendly to type checking. And it doesn't work with positional-only arguments. How is the sentinel value private information or an implementation detail? It's part of the API. It should be clearly documented. If nothing else, it's can be inspected and discovered.
As far as changing the defaults, consider:
def f(x=3): return x ... f() 3 f.__defaults__=(42,) f() 42
The current PEP design does not provide for this functionality for late-bound defaults. Remember, though: the true comparison should be something like this:
_SENTINEL = object() def f(x=_SENTINEL): if x is _SENTINEL: x = [] return x
Can you change that from a new empty list to something else? No. All you can do, by mutating the function's dunders, is change the sentinel, which is actually irrelevant to the function's true behaviour. You cannot change the true default. It is none the less true that default late-bound values cannot be modified. Correct? Early-bound ones can. Consider also this form:
default_timeout = 500 def connect(s, timeout=default_timeout): ... def read(s, timeout=default_timeout): ... def write(s, msg, timeout=default_timeout): ...
You can now, if you go to some effort, replace the default in every function. Or you can do this, and not go to any effort at all:
def read(s, timeout=>default_timeout): ...
The true default is now exactly what the function signature says. And if you really want to, you CAN change read.__defaults__ to have an actual early-bound default, which means it will then never check the default timeout.
Introspection is no worse in this way than writing out the code longhand. It is significantly better, because even though you can't change it from a latebound default_timeout to a latebound read_timeout, you can at least see the value with external tools. You can't see that if the default is replaced in the body of the function.
I realize the response will be that code shouldn't need to do these things, but I do not think we should be adding features to python that limit what introspections and runtime modifications user code can do. The response is more that the code CAN'T do these things, by definition. To the extent that you already can, you still can. To the extent that you should be able to, you are still able to. (And more. There are things you're capable of with PEP 671 that you definitely shouldn't do in normal code.)
This is a tautology. You can't do these things if 671 is accepted because they will defined as not doable by 671. That's a strike against it. My stance is that it should be possible, and a proposal that makes them not possible with late-bound arguments is deficient.
A classic example of this is PEP 362 function signature objects. I don't think we should be adding parameter types that cannot be represented in a Signature, although of course a Signature might need to be extended to support new features. Signature objects were added for a reason (see the PEP), and I don't think we should just say "well, that's not important for this new feature". Also note that over time we've removed restrictions on Signatures (see, for example, Argument Clinic). So I don't think adding restrictions is the direction we want to go in. Same again. If you consider the equivalent to be a line of code in the function body, then the signature has become MASSIVELY more useful. Instead of simply seeing "x=<object object at 0x7fba1b318690>", you can see "x=>[]" and be able to see what the value would be. It's primarily human-readable (although you could eval it), but that's still a lot better than seeing a meaningless sentinel. And yes, you can make help() more readable by using a nicely named sentinel, but then you have to go to a lot more effort in your code, worry about pickleability, etc, etc. Using a late-bound default lets you see the true default, not a sentinel.
We're going to have to disagree about this. I think it's critical that Signature objects be usable with all types of defaults. And having the default value available as a string isn't very useful, except for displaying help text. It wouldn't be possible to create a new Signature object from an existing Signature object that contains a late-bound argument (say, to create a new Signature with an additional argument). At least I haven't seen how it would be possible, since the PEP makes no mention of Signature objects. Which it definitely should, even if only to say "late-bound arguments are not designed to work with Signature objects". Sentinels are not meaningless. I think you're alienating people every time you suggest they are. In any event, I've expended enough volunteer time discussing this for now, so I'm going to drop off. I'm against another way to specify function argument defaults, and I'm against some of the decisions that PEP 671 has made. I've stated those objections. I'll argue against it further when the SC is asked to consider it. Eric