On Sun, Oct 24, 2021 at 8:56 AM <2qdxy4rzwzuui...@potatochowder.com> wrote:
>
> On 2021-10-24 at 06:54:36 +1100,
> Chris Angelico <ros...@gmail.com> wrote:
>
> > On Sun, Oct 24, 2021 at 6:18 AM <2qdxy4rzwzuui...@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.

"Function header" is not about time, it's about place.

def spam(x: int, y: int) -> Thingy:
    ...

The annotations might be evaluated at function definition time
(although some proposals are looking at changing that), but they may
also be evaluated long before that, during a static analysis phase. We
don't need a separate place in the code for "stuff that runs during
static analysis", because logically, it's all about those same
function parameters.

Late-bound argument defaults are still argument defaults. When you're
thinking about how you call the function, what matters is "this
argument is optional, and if you don't specify it, this is what
happens". Sometimes the definition of 'this' is a specific value
(calculated by evaluating an expression at definition time).
Sometimes, it's some other behaviour, defined by the function itself.
All this proposal does is make the most common of those into a new
option: defining it as an expression.

> > 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.

It currently is, due to a technical limitation. There's no particular
reason that it HAS to be. For instance, consider these two:

def popitem(items, which=-1): ...
def popitem(items, which=len(items) - 1): ...

Both of them allow you to omit the argument and get the last one. The
first one defines it with a simple value and relies on the fact that
you can subscript lists with -1 to get the last element; the second
doesn't currently work. Is there a fundamental difference between
them, or only a technical one?

> > 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.

It's mainly about what [] means and when it's evaluated. Either way,
self.items is a list. The only difference is whether instantiating the
class creates a new list, or you keep referring to the same one every
time.

> 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).

Yes. It is the exact same distinction as early-bound or late-bound arguments.

> > 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())

I don't have any good examples where this happens, so it's hard to
argue, but I definitely don't see any advantage in the second one.
It's almost identically as repetitive, and offers very little
advantage.

If expensive() is a method call, I'd just tack an lru_cache onto it
and be done with it. Having a wrapper like this just looks like a toy,
and one that's hard to argue on the basis of. Would need a real
example.

ChrisA
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/OUDKGZGTLBBAJVPOIJ7ZQ4QRFEIUYWKN/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to