On 2021-12-08 23:22, Chris Angelico wrote:
On Thu, Dec 9, 2021 at 5:54 PM Brendan Barnwell <brenbarn@brenbarn.net> wrote:
On 2021-12-08 20:36, Chris Angelico wrote:
Remember, though: The comparison should be to a function that looks like this:
def f(a=[], b=_SENTINEL1, c=_SENTINEL2, d=_SENTINEL3): if b is _SENTINEL1: b = {} if c is _SENTINEL2: c = some_function(a, b) if d is _SENTINEL3: d = other_function(a, b, c)
If you find the long-hand form more readable, use the long-hand form! It's not going away. But the introspectability is no better or worse for these two. The late-bound defaults "{}", "some_function(a, b)", and "other_function(a, b, c)" do not exist as objects here. Using PEP 671's syntax, they would at least exist as string constants, allowing you to visually see what would happen (and, for instance, see that in help() and inspect.signature).
I don't want to get bogged down in terminology but I am becoming increasingly frustrated by you using the term "default" both for things that are values and things that are not, as if there is no difference between them.
That's absolutely correct: I am using the term "default" for anything that provides a default for an optional argument that was omitted. In some cases, they are default values. In other cases, they are default expressions. If your docstring says "omitting d will use the length of a", then the default for d is len(a).
Your definition is somewhat circular, because you say that a default is "anything that provides a default". But that says "default" again. So what is a default? By your definition, any arbitrary code inside a function body that eventually assigns something to an argument name is a default. (It is not clear to me whether you would consider some code a default if it may or may not assign a value to an argument, depending on some conditions.) So I don't agree with that definition. That can be default BEHAVIOR, but it is function behavior; it is not an argument default.
There are no late-bound defaults here, in the sense that I mean, which as I said before has to do with default VALUES. There is just code in the function body that does stuff. I am fine with code in a function body doing stuff, but that is the purview of the function and not the argument. An individual ARGUMENT having a default VALUE is not the same as the FUNCTION defining BEHAVIOR to deal with a missing value for an argument.
In a technical sense, the default value for b is _SENTINEL1, but would you describe that in the docstring, or would you say that omitting b would use a new empty dictionary? You're getting bogged down, not in terminology, but in mechanics. At an abstract level, the default for that argument is whatever would be used if the argument is omitted.
I don't agree. At an abstract level, there is no clear dividing line between what you call an argument default and just "arbitrary behavior of the function". What if "what would be used" depends on random numbers or data from some external source? Or, again, what you are describing is not an argument default (in my conception). It may be the BEHAVIOR of the function to do a certain thing (like use a certain value in place of an omitted argument) but unless that behavior is segmented and associated with the argument itself (not merely part of the function's code flow) I don't consider it an argument default. As for the docstring, yes, I might well mention _SENTINEL1 in the docstring. I certainly wouldn't see anything wrong with that. That's what the default is. Again, the function may USE that value in some way, but that doesn't mean that's not what the default is; it just means the function conditions its behavior on its argument value, as any function may do on any argument value, omitted or not. I get the impression you think that in a case like that the default "really is" something else defined in the body of the function, but again I disagree. The default really is _SENTINEL1. Conceptually we may understand that the function will use that value in a special way, but that is not really any different than understanding that passing "r" to open() will open the file for reading while passing "w" will open it for writing. It's just that to know how to use a function you need to know more than the default values of the arguments; you need to know what they MEAN, and (at least with current technology :-) we have no way of deriving that from the source code. You're quite right that "at an abstract level" it may be the case that the default behavior is to do a certain thing, but I guess one way to state my position would be that I think that is TOO abstract of a level to worry about representing in code. At an abstract level I may say "this function computes the number of paths of length N between the given nodes in the given graph", but I don't expect that to be mentioned in the signature or automatically provided in the docstring. I would certainly WRITE it in the docstring, but I don't expect Python to deduce that "abstract" level of meaning from code annotations and write that docstring for me. In other words, I think mechanics is the right level to be at here. We cannot hope to capture the abstract level that you're describing, and I think doing so will just muddle matters. At an abstract level we say ""this function computes the number of paths of length N between the given nodes in the given graph" but what we write is `def n_paths(graph, node1, node2)`. I don't see any reason we need to be able to write `len(x)` in the function signature just because at that abstract level we think of it as something that may be computed later. This is especially so because, as I mentioned above, there is no clear line separating "code that we can write in a function to assign a default value to an argument" and "code we can write in a function for other purposes" --- and thus there is no way to distinguish behavior that is "tied" to a particular argument from just code that uses any old combination of values it wants. We write code in terms of instrumental units which necessarily are at a slightly more concrete level than the purely abstract or conceptual realm of "what this function does". For instance, objects (which, until now, every function argument, default or not, is). I don't see any reason why late-bound defaults should be represented in code in a way that attempts to capture this abstract level when other aspects of functions are not and cannot be.
To justify this, please explain WHY it is so important for defaults to all be objects. Not just "that's how they are now", but why that is an important feature.
Because I'm used to reasoning about Python code in terms of operations on objects, and so are a lot of other people. Everything I or anyone else currently needs to know about how functions and their arguments work in Python can be thought of in terms of objects. Why add a new complication? I mean, okay, maybe that is really just saying "that's how they are now", although it's more like "right now defaults are part of the big set of things that are objects and this change would peel them off and create a new type of thing". But apart from that, I think part of what makes Python a nice language is the way that many language functions are represented in terms of objects, for instance the iterator and descriptor protocols. The idea of the object as a locus of functionality --- that the way you "do something" (like loop or access an attribute) is represented as "get an object representing the functionality and call certain methods on it" --- gives unity to many Python features. It's true that's a pretty abstract reason, but I think it's a legit one. Also, let's remember that burden of evidence is really the other way around here. Can you really explain WHY it is so important for late-bound defaults to be represented with special syntax of the type you propose? Not only can you not rely on "that's how they are now" (because they're not), but in fact you must overcome the reverse argument, namely that people have been doing pretty well with just early-bound defaults for decades now. In other words, even if it is not particularly important for defaults to be objects, it may still be more important than being able to write a "late-bound default" (aka "behavior in the function body") in the signature. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown