On Sun, Dec 5, 2021 at 11:34 AM Steven D'Aprano <st...@pearwood.info> wrote:
>
> On Sat, Dec 04, 2021 at 10:50:14PM +1100, Chris Angelico wrote:
>
> > > syntactic sugar for this:
> > >
> > >     def f(b, x=lambda b: a+b): ...
> > >
> > > except that the lambda has the LB flag set.
> >
> > Okay. So the references to 'a' and 'b' here are one more level of
> > function inside the actual function we're defining, which means you're
> > paying the price of nonlocals just to be able to late-evaluate
> > defaults. Not a deal-breaker, but that is a notable cost (every
> > reference to them inside the function will be slower).
>
> How much slower? By my tests:
>
> - access to globals is 25% more expensive than access to locals;
> - access to globals is 19% more expensive than nonlocals;
> - and nonlocals are 6% more expensive than locals.
>
> Or if you do the calculation the other way (the percentages don't match
> because the denominators are different):
>
> - locals are 20% faster than globals;
> - and 5% faster than nonlocals;
> - nonlocals are 16% faster than globals.
>
> Premature optimization is the root of all evil.
>
> We would be better off spending effort making nonlocals faster for
> everyone than throwing out desirable features and a cleaner design just
> to save 5% on a microbenchmark.

Fair, but the desirable feature can be achieved without this cost, and
IMO your design isn't cleaner than the one I'm already using, and 5%
is a lot for no benefit.

> [...]
> > What this does mean, though, is that there are "magic objects" that
> > cannot be used like other objects.
>
> NotImplemented says hello :-)

Good point. Still, I don't think we want more magic like that.

> And if you still think that we should care, we can come up with a more
> complex trigger condition:
>
> - the parameter was flagged as using a late-default;
> - AND the default is a LB function.
>
> Problem solved. Now you can use LB functions as early-bound defaults,
> and all it costs is to record and check a flag for each parameter. Is it
> worth it? Dunno.

Uhh.... so..... the parameter has to be flagged AND the value has to
be flagged? My current proposal just flags the parameter. So I ask
again: what are you gaining by this change? You've come right back to
where you started, and added extra costs and requirements, all for....
what?

> [...]
> > > The default expression is just a function (with the new LB flag set). So
> > > we can inspect its name, its arguments, its cell variables, etc:
> > >
> > >     >>> default_expression.__closure__
> > >     (<cell at 0x7fc945de74f0: int object at 0x7fc94614c0f0>,)
> > >
> > > We can do anything that we could do with any other other function object.
> >
> > Yup. As long as it doesn't include any assignment expressions, or
> > anything else that would behave differently.
>
> I don't get what you mean here. Functions with the walrus operator are
> still just functions that we can introspect:
>
> >>> f = lambda a, b: (len(w:=str(a))+b)*w
> >>> f('spam', 2)
> 'spamspamspamspamspamspam'
> >>> f.__code__
> <code object <lambda> at 0x7fc945e07c00, file "<stdin>", line 1>
>
> What sort of "behave differently" do you think would prevent us from
> introspecting the function object? "Differently" from what?

Wrapping it in a function means the walrus would assign in that
function's context, not the outer function. I think it'd be surprising
if this works:

def f(x=>(a:=1)+a): # default is 2

but this doesn't:

def g(x=>(a:=1), y=>a): # default is UnboundLocalError

It's not a showstopper, but it is most definitely surprising.

The obvious solution is to say that, in this context, a is a nonlocal.
But this raises a new problem: The function object, when created, MUST
know its context. A code object says "this is a nonlocal", and a
function object says "when I'm called, this is my context". Which
means you can't have a function object that gets called externally,
because it's the code, not the function, that is what you need here.
And that means it's not directly executable, but it needs a context.

So, once again, we come right back around to what I have already: code
that you can't lift out and call externally. The difference is that,
by your proposal, there's a lot more overhead, for the benefit of
maybe under some very specific circumstances being able to synthesize
the result.

> > We also need to
> > have these special functions that get stored as separate code objects.
>
> That's not a cost, that's a feature. Seriously. We're doing that so that
> we can introspect them individually, not just as the source string, but
> as actual callable objects that can be:
>
> - introspected;
>
> - tested;
>
> - monkey-patched and modified in place (to the degree that any function
>   can be modified, which is not a lot);
>
> - copied or replaced with a new function.
>
> Testing is probably the big one. Test frameworks will soon develop a way
> to let you write tests to confirm that your late bound defaults do what
> you expect them to do.
>
> That's trivial for `arg=[]` expressions, but for complex expressions in
> complex functions, being able to isolate them for testing is a big plus.
>

I'm still not convinced that it's as useful as you say. Compare these
append-and-return functions:

def build1(value, lst=None):
    if lst is None: lst = []
    lst.append(value)
    return lst

_SENTINEL = object()
def build2(value, lst=_SENTINEL):
    if lst is _SENTINEL: lst = []
    lst.append(value)
    return lst

def hidden_sentinel():
    _SENTINEL = object()
    def build3(value, lst=_SENTINEL):
        if lst is _SENTINEL: lst = []
        lst.append(value)
        return lst
    return build3
build3 = hidden_sentinel()

def build4(value, *optional_lst):
    if len(optional_lst) > 1: raise TypeError("too many args")
    if not optional_lst: optional_lst = [[]]
    optional_lst[0].append(value)
    return optional_lst[0]

def build5(value, lst=>[]):
    lst.append(value)
    return lst

(Have I missed any other ways to do this?)

In which of them can you introspect the []? Three of them have an
externally-visible sentinel, but you can't usefully change it in any
way. You can look at it, and you'll see None or "<object object at
0xYourCapitalCity>", but that doesn't tell you that you'll get a new
empty list. In which of them do you have a thing you can call that
will generate an equivalent empty list?

In which can you monkey-patch or modify-in-place the []? Not a big
deal though, you admit that there wouldn't be much monkey-patching in
any form. But you get none whatsoever with other idioms.

In which of these can you separately test the []? Do any current test
frameworks have a way to let you write tests to make sure that these
do what you expect?

In which of these can you copy the [] into another context, or replace
the [] with something else?

Why are these such major problems for build5 if they're not for the other four?

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/SKQS4LXFPUESCSFWNTGUSKTTJF4XQL2S/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to