On Wed, 22 Jun 2022 at 11:59, David Mertz, Ph.D. firstname.lastname@example.org wrote:
Thanks Carl and Chris. After reading your comments, and thinking some more about it, I agree you are both correct that it only makes sense to have a DeferredObject act like a closure in the scope of its creation. That really should suffice for anything I could sensibly want; it's enough for Dask, and it's enough for functional programming languages with laziness.
Moreover, the limited laziness that Python currently has also follows that rule. I also haven't gotten to writing that needed section of the PEP, but I need to address generators and boolean shortcutting as limited kinds of laziness. Obviously, my PEP is much broader than those, but generators also use lexical scope not dynamic scope.
Possibly in my defense, I think Carl's PEP 690 can do the same thing. :-)
I’m not sure what you mean. I don’t think there’s any way PEP 690 can introduce dynamic scoping like this. Can you give an example?
Let me just withdraw that. I think I might be able to construct something where a module's top-level code does something perverse with the call stack where it winds up getting evaluated that can act like dynamic scoping. I might be wrong about that; but even if it's technically true, it would need very abnormal abuse of the language (as bad as the lovely zero-argument super() does :-)).
So then a deferred object is basically a lambda function that calls itself when referenced. That's reasonable, and broadly sane...
I don't think this actually has much effect on encompassing late-binding of default arguments, other than needing to say the arguments contribute to the scope on a left-to-right basis. But actually, despite writing the section because of recent discussion, PEP-671 has little to do with why I want generalized deferreds. I've suggested it on a number of occasions long before PEP-671 existed (but admittedly always as a passing thought, not as a detailed proposal... which my draft still is not either).
On one small point Chris mentions, I realized before he commented that there was no need to actually rebind the deferred argument to force evaluation of its default. Simply mentioning it would force the evaluation. I found something similar in PEP-690 that reminded me that doing this is plenty:
def func(items=, n=later len(items)): n # Evaluate the Deferred items.append("Hello") print(n)
Of course, that's an extra first line of the function for the motivating case of emulating the sentinel pattern. But it also does let us *decide* when each deferred gets "fixed in place". I.e. maybe a, b, and c are all `later` arguments. We could make the first line `a, b` but only reference `c` at some later point where it was appropriate to the logic we wanted.
... but it doesn't work for this case. If a deferred is defined by its creation context, then the names 'len' and 'items' will be looked up in the surrounding scope, NOT the function's. It would be like this:
def func(items=, n=lambda: len(items)): n = n()
which, in turn, is similar to this:
_default = lambda: len(items) def func(items=, n=None): if n is None: n = _default n = n()
and it's fairly clear that this has the wrong scope. That's why PEP 671 has the distinction that the late-bound default is scoped within the function body, not its definition. Otherwise, this doesn't work.