On 2021-10-24 at 06:54:36 +1100,
Chris Angelico
On Sun, Oct 24, 2021 at 6:18 AM <2QdxY4RzWzUUiLuE@potatochowder.com> wrote:
The expression would be evaluated in the function's context, having available to it everything that the function has. Notably, this is NOT the same as the context of the function definition, but this is only rarely going to be significant (eg class methods where a bare name in an early-bound argument default would come from class scope, but the same bare name would come from local scope if late-bound).
The purpose of this change is to have the function header define, as fully as possible, the function's arguments. Burying part of that definition inside the function is arbitrary and unnecessary.
Those two paragraphs contradict each other. If the expression is evaluated in the function's context, then said evaluation is (by definition?) part of the function and not part of its argumens.
The function header is a syntactic construct - the "def" line, any decorators, annotations, etc.
If you mean that def statements and decorators run at compile time, then I agree. If you mean something else, then I don't understand.
But for the late-binding expressions to be useful, they MUST be evaluated in the context of the function body, not its definition. That's the only way that expressions like len(a) can be of value. (Admittedly, this feature would have some value even without that, but it would be extremely surprising and restrictive.)
I think we're saying the same thing, but drawing different conclusions. I agree with everything in the first paragraph I quoted above, but I can't make the leap to claiming that late binding is part of defining the function's arguments. You say "late binding of function arguments"; I say "the part of the function that translates the arguments into something useful for the algorithn the function encapsulates."
As a separate matter, are following (admittedly toy) functions (a) an infinite confusion factory, or (b) a teaching moment?
def f1(l=[]): l.append(4) return l
def f2(l=:[]): l.append(4) return l
Teaching moment. Currently, the equivalent second function would be this:
def f2(l=None): if l is None: l = [] l.append(4) return l
And the whole "early bind or late bind" question is there just the same; the only difference is that the late binding happens somewhere inside the function body, instead of being visible as part of the function's header. (In this toy example, it's the very next line, which isn't a major problem; but in real-world examples, it's often buried deeper in the function, and it's not obvious that passing None really is the same as passing the array's length, or using a system random number generator, or constructing a new list, or whatever it is.)
It's only not obvious if the documentation is lacking, or the tools are lacking, or the programmer is lacking. The deeper "it" is in the function, the more you make my point that it's part of the function itself and not part of setting up the arguments.
This is, ultimately, the same teaching moment that you can get in classes:
class X: items = [] def add_item(self, item): self.items.append(item)
class Y: def __init__(self): self.items = [] def add_item(self, item): self.items.append(item)
Understanding these distinctions is crucial to understanding what your code is doing. There's no getting away from that.
Understanding the difference between defining a class and instantiating that class is crucial, as is noticing the very different source code contexts in which X.items and self.item are created. I agree. Stuff in class definitions (X.items, X.add_item, Y.__init__, Y.add_item) happens when X is created, arguably at compile time. The code inside the function suites (looking up and otherwise manipulating self.items) happens later, arguably at run-time. In f1, everything in the "def" statement happens when f1 is defined. In f2, part of the "def" statement (i.e., defining f2) happens when f2 is defined (at compile-time), but the other part (the logic surrounding l and its default value) happens when f2 is called (at run-time).
I'm aware that blessing this with nice syntax will likely lead to a lot of people (a) using late-binding everywhere, even if it's unnecessary; or (b) using early-binding, but then treating late-binding as a magic bandaid that fixes problems if you apply it in the right places. Programmers are lazy. We don't always go to the effort of understanding what things truly do. But we can't shackle ourselves just because some people will misuse a feature - we have plenty of footguns in every language, and it's understood that programmers should be allowed to use them if they choose.
I won't disagree. Maybe it's just that I am the opposite of sympathetic to the itches (and those itches' underlying causes) that this particular potential footgun scratches. Curiously, for many of the same reasons, I think I'm with you that: def get_expensive(self): if not self.expensive: self.expensive = expensive() return self.expensive is better (or at least not worse) than: def get_expensive(self): return self.expensive or (self.expensive := expensive())