On Fri, Dec 03, 2021 at 02:10:12AM +1100, Chris Angelico wrote:

> > > Unfortunately not, since the default expression could refer to other
> > > parameters, or closure variables, or anything else from the context of
> > > the called function. So you won't be able to externally evaluate it.
> >
> > Why not? Functions can do all those things: refer to other variables, or
> > closures, or anything else. You can call functions. Are you sure that
> > this limitation of the default expression is not just a limitation of
> > your implementation?
> 
> def f():
>     a = 1
>     def f(b, c=>a+b): return c
>     a = 2
>     return f
> 
> If there were a function to represent the late-bound default value for
> c, what parameters should it accept?

I'm not saying that it *must* be a function. It could be a bare code 
object, that is `eval()`ed. Or something completely new. Dunno.

But you're saying something is impossible, and that seems implausible to 
me, because things that seems *very similar* are totally possible.


> How would you externally evaluate this?


inner = f()  # Get the inner function.
default = inner.__code__.__late_defaults__.wibble[1]  # whatever
try:
    value = default()
except NameError:
    # Well, what did you expect to happen?
    value = eval(default.__code__, globals(), {'a': 101, 'b': 202})


Or something. The point is, rather than dismissing the possibility 
outright, this should be something we discuss, and carefully consider, 
before the PEP is complete.


> And also: what do you gain by it being a function, other than a
> lot of unnecessary overhead?

Nicer introspection.

Brendan goes from strongly opposed to the PEP to its biggest and most 
tireless supporter *wink*

Cleaner separation of concerns: the defaults get handled independently 
of the function body.

Plays nice with other tools that (say) use byte-code manipulation on 
the function body.



> And it is potentially a LOT of unnecessary overhead. Consider this edge case:
> 
> def f(a, b=>c:=len(a)): ...
> 
> In what context should the name c be bound?

The obvious (which is not necessarily correct) answer is, the same scope 
that the expression is evaluated in, unless its declared global or 
nonlocal. (Which is only relevant if the function f() is nested in 
another function.)

With regular defaults, the expression is evaluated at function 
definition time, and c gets bound to the surrounding scope.

With late-bound defaults, the expression is evaluated at function call 
time, in the scope of f()'s locals. So c would be a local.

Or the other obvious answer is that c will always be in the surrounding 
scope, for both early and late bound defaults.

Consider the existence of walrus expressions in comprehensions:


>>> def demo(a, b=((w:=i**2)*str(w) for i in range(5))):
...     return b
... 
>>> it = demo(None)
>>> next(it)
''
>>> next(it)
'1'
>>> next(it)
'4444'
>>> w
4

So there is precedent for having function-like entities (in this case, a 
comprehension) exposing their walrus variables in the surrounding scope.

The third obvious answer is that if either the decision or the 
implementation is really too hard, then make it a syntax error for now, 
and revisit it in the future.



> If there's a function for
> the evaluation of b, then that implies making c a closure cell, just
> for the sake of that. Every reference to c anywhere in the function
> (not just where it's set to len(a), but anywhere in f()) has to
> dereference that.

Okay. Is this a problem?

If it really is a problem, then make it a syntax error to use walrus 
expressions inside late bound defaults.


> It's a massive amount of completely unnecessary overhead AND a
> difficult question of which parts belong in the closure and which
> parts belong as parameters, which means that this is nearly impossible
> to define usefully.

I've given you two useful definitions.

Its not clear what overhead you are worried about.

Accessing variables in cells is almost as fast as accessing locals, but 
even if they were as slow as globals, premature optimization is the root 
of all evil. Globals are fast enough.

Or are you worried about the memory overhead of the closures? The extra 
cost of fetching and calling the functions when evaluating the defaults? 
None of these things seem to be good reasons to dismiss the idea that 
default expressions should be independent of the function body.

"Using a walrus expression in the default expression will make your 
function 3% slower and 1% larger, so therefore we must not make the 
default expression an introspectable code object..."


> > > I'm still unsure whether this is a cool feature or an utter abomination:
> > >
> > > >>> def f(x=...):
> > > ...     try: print("You passed x as", x)
> > > ...     except UnboundLocalError: print("You didn't pass x")
> > > ...
> > > >>> f.__defaults_extra__ = ("n/a",)
> > > >>> f(42)
> > > You passed x as 42
> > > >>> f()
> > > You didn't pass x

[...]
> That's not what the example shows. It shows that changing dunder
> attributes can do this. I'm not sure why you think that the
> implementation is as restricted as you imply. The assignment to
> __defaults_extra__ is kinda significant here :)

Ah, well that is not so clear to people who aren't as immersed in the 
implementation as you :-)

Messing about with function dunders can do weird shit:

>>> def func(a=1, b=2):
...     return a+b
... 
>>> func.__defaults__ = (1, 2, 3, 4, 5)
>>> func()
9

I wouldn't worry about it.


-- 
Steve
_______________________________________________
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/2K62MH3JAJANDUTTTH46RK76ESV4MREM/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to