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