On Fri, Dec 3, 2021 at 2:30 PM Brendan Barnwell <brenbarn@brenbarn.net> wrote:
On 2021-12-02 15:40, Chris Angelico wrote:
Actually, no. I want to put the default arguments into the signature, and the body in the body. The distinction currently has a technical restriction that means that, in certain circumstances, what belongs in the signature has to be hacked into the body. I'm trying to make it so that those can be put where they belong.
Chris, I know this is probably not your intention, but I feel the discussion is continually muddle by you just saying "default arguments" as if everyone agrees on what those are and the issue is just where to put them. But clearly that is not the case. You seem to take it for granted that "len(x) evaluated when the function is called" "is" a "default argument" in the same sense that an early-bound default like the number 3 is. I do not agree, and it's pretty clear David Mertz does not agree, and I think there are others here who also do not agree.
It's not as is there is some pre-existing notion of "default argument" in Python and you are just proposing to add an implementation of it that was left out due to some kind of oversight. Your proposal is CHANGING the idea of what a default argument is. I get that it's natural to refer to your new proposal as "default arguments" but I've now seen a number of messages here where you say "but no we should do X because this is a default argument", taking for granted that what you're talking about is already agreed to be a default argument. (No doubt I and many others are also guilty of similar missteps by saying "default argument" as a shorthand for something or other, and I'll try to be careful about it myself.)
Some functions most assuredly DO have a pre-existing notion of "default argument". Some do not. Allow me to give a few examples: def getattr(object, name, default): Omitting the third argument does not have a default. It will cause different behaviour (getattr will raise an exception). def dict.get(key, default=None): Omitting the last argument behaves exactly as if you passed None. This has a default; the function behaves identically whether you pass an argument or not, and you can determine what the behaviour would be if you do. (These two are a little confusing in that they use the name "default", but the fact is that default arguments are often used for defaults, surprise surprise. So I'm going to be reusing that word a lot.) def open(file, mode='r', encoding=????): Omitting mode is exactly the same as passing 'r'. Omitting encoding is exactly the same as passing locale.getpreferredencoding(False). Both of these have real defaults. def bisect.bisect(a, x, lo=0, hi=len(a)): Omitting lo is exactly the same as passing 0. Omitting hi is exactly the same as passing len(a). Both of these have real defaults. For parameters that do not have defaults, PEP 671 has nothing to say. Continue doing what you're already doing (whether that's a sentinel, or *args, or whatever), as there's nothing needing to be changed. For parameters whose defaults are simple constants, PEP 671 also has nothing to say. Continue defining them as early-bound defaults, and the behaviour will not change. The difference comes with those arguments whose true default is calculated in some way, or is dynamic. These really truly do have default values, and there is absolutely no behavioural difference between hi=len(a) and omitting the hi parameter to bisect(). Due to a technical limitation, though, the documentation for bisect() has to explain this in the docstring rather than the function signature. Would this code pass review? def spaminate(msg, times=None): if times is None: times = 50 ... Unless there's a very VERY good reason for using None as the default, shouldn't it use 50 in the signature? It would be a clearer declaration of intent: omitting this argument is the same as passing 50.
By my definition as of now, a default argument has to be an object. Thus what your proposal envisions are not default arguments. You can't just say "I want to do this with default arguments". You need to provide an argument for why we should even consider these to be default arguments at all.
Yes, that is a current, and technical, limitation. Consider this: https://en.wikipedia.org/wiki/Default_argument#Evaluation Python is *notable* for always evaluating default arguments just once. The concept of default args, as described there and as understood by anyone that I have asked (mainly students, which I admit isn't an unbiased sample, but nothing is), is that they should be evaluated at call time. My argument for why they should be considered default arguments is that, aside from Python being unable to represent them, they *are* precisely what default arguments are. Suppose you're on the ECMAScript committee, and someone proposes that the language support bignums. (That's not truly hypothetical, incidentally - I saw references to such a proposal.) Would you argue that 9007199254740993 is not really a number, on the basis that current ECMAScript cannot represent it? Would you force someone to prove that it is really a number? It's only special because of implementation restrictions.
Perhaps a way of stating this without reference to arguments is this: you want to put code in the function signature but not have it executed until the function is called. I do not agree with that choice. The function signature and the function body are different syntactic environments with different semantics. Everything in the function signature should be executed when the function is defined (although of course that execution can result in an object which contains deferred behavior itself, like if we pass a function as an argument to another).
That's reasonable, but I disagree from a perspective of practicality: logically and usefully, it is extremely helpful to be able to describe arguments that way. Passing positional parameters is approximately equivalent to: def func(*args): """func(a, b, c=1, d=a+b)""" a, args = args b, args = args if args: c, args = args else: c = 1 if args: d, args = args else: d = a + b If you try to explain it to someone who's learning about default argument values, do you first have to explain that they get evaluated and recorded somewhere, and these indescribable values are what's actually assigned? Or would you describe it mostly like this (or as "if c is omitted: c = 1", which comes to the same thing)? Argument default snapshotting is an incredibly helpful performance advantage, but it isn't inherent to the very concept of default arguments.
Only the function body should be executed when the function is called. If we want to provide some way to augment the function body to assign values to missing arguments, I wouldn't rule that out completely, but in my view the function signature is not an available space for that. The function signature is ONLY for things that happen when the function is defined. It is too confusing to have the function signature mix definition-time and call-time behavior.
The trouble is that the function signature MUST be the one and only place where you figure out whether the argument is optional or mandatory. Otherwise there is an endless sea of debugging nightmares - just ask anyone who works regularly in JavaScript, where all arguments are optional and will default to the special value 'undefined' if omitted. So you'd need to separate "this is optional" from "if omitted, do this". That might be a possibility, but it would need two separate compiler features: def func(a, b, c=1, d=?): if unset d: d = a+b where "unset N" is a unary operator which returns True if the name would raise, False if not. I'm not proposing that, but if someone wants to, I would be happy to share my reference implementation, since 90% of the code is actually there :) ChrisA