Ah, I think I get you. The problem is that code is in the def line but is only executed when the function is called, is that correct? Because the code would be "re-executed" just as much if it were written in the function body. It's executed (at most) once for each call to the function, just like the ternary's side is.
Hmmm, is that really the root of our disagreement? That's surprising. :-) Anyway, yes. The function body and the function signature are two different things. Function definition time and function call time are two different things.
I suppose that's a consideration, but it's not nearly as strong in practice as you might think. A lot of people aren't even aware of the difference between compilation time and definition time (even people on this list have made that mistake). Function default args are executed when the function is defined, not when it's called, and that's something that changes with this proposal; but there are many other subtleties to execution order and timing that don't really matter in practice.
That is a surprising statement to me. I consider the distinction between definition time and call time to be quite fundamental to Python. At a minimum one has to understand that the function body isn't executed when the function is defined (i.e., you have to call it later to "activate" the defined behavior). This is something I've seen beginner programmers struggle with. Understanding the difference between def time and call time is also important for understanding how decorators work. It's why most decorators have a structure that is confusing to many newcomers, where the decorator itself is a function that defines another function and then returns it. It has to be this way because of the distinction between the time the decorated function is defined (which is when the decorator is called) and the time it is called (which is when the "inner function" that the decorator defines is called). Or, to put it bluntly, I think people just need to understand the difference between def time and call time to understand Python's semantics. Of course even experienced programmers may sometimes get mixed up in a particular case, but I fully reject the idea that this distinction is some kind of obscure technicality that we can ignore or blur by including call-time behavior in the def-time signature. The distinction is fundamental to how functions work in Python.
Perhaps the key point here is to consider function decorators. We could avoid them altogether:
def f(): @deco def g(x): ...
def h(x): ... h = deco(h)
But as well as having the name replication problem, this buries important information down in the body of the surrounding code, rather than putting it at the signature of g/h where it belongs. Even though, semantically, this is actually part of the body of f, we want to be able to read it as part of the signature of g. Logically and conceptually, it is part of the signature. Now compare these two:
Decorators aren't part of the signature of the function they decorate. deco is not part of the signature of g. A decorator is sort of like a "transform" applied to the function after it is defined. It's true we have decorators and like them because they allow us to put that transform up front rather than at the bottom, but that's not because of anything about signatures. It's just because some transforms are nice to know about up front.
def f2(): _SENTINEL = object() def g(x=_SENTINEL): if x is _SENTINEL: x = [] ...
def h(x=>[]): ...
Which one has its signature where its signature belongs? Yes, semantically, the construction of the empty list happens at function call time, not at definition time. But what you're saying is: if there are no args passed, behave as if a new empty list was passed. That's part of the signature.
Again, I disagree. Any behavior that happens at call time is not part of the signature. The signature specifies the function's parameter names and optionally maps any or all of them to objects to be used in case they are not passed at call time.
In neither case will you find an object representing the expression [] in the function's signature, because that's not an object, it's an instruction to build an empty list. In the case of g, you can find a meaningless and useless object stashed away in __defaults__, but that doesn't tell you anything about the true behaviour of the function. At least in the case of h, you can find the descriptive string "[]" stashed there, which can tell a human what's happening.
You say "In the case of g, you can find a meaningless and useless object stashed away in __defaults__, but that doesn't tell you anything about the true behaviour of the function." Yes. And that's fine. As I've mentioned in other messages in these threads, I am totally fine with the idea that there is stuff in the function signature that does not completely characterize the function's behavior. The place for the function author to explain what the functions defaults mean is in documentation, and the place for the user to learn what the defaults mean is also the documentation. I think again that another area of our disagreement is the relationship between the function signature and "the true behavior of the function". To me the signature is to the "true behavior" as the cover of a thick tome to its contents. The "true behavior" of a function can be arbitrarily complex in myriad ways which we can never dream of communicating via the signature. The signature cannot hope to even begin to convey more than an echo of a whisper of a shadowy reflection of "the true behavior of the function". We should not worry ourselves in the slightest about the fact that "the true behavior of the function" may not be transparently represented in the signature. Sure, we should choose good names for the parameters, but that's about all I expect from a function signature. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown