Back quotes have been freed from a syntactic sugar for repr since the first version of python3, I think it will be perfect to be repurposed for deferred expressions.
Use cases:
1. Late-bound function arguments
def fn(x: int, y: int, z: int = `x + y`):
print(z) # expr will be evaluated here
fn(1, 2) # 3
def fn(el: object, z: list = `[]`):
z.append(el)
print(z)
fn(1) # [1]
fn(2) # [2]
def task1(next_task=`task2`): ...
def task2(next_task=`task1`): ...
# No NameError!
2. A new approach of declaring zero-argument lambdas
P.S. This is fundamentally how it behaves: a lambda function with zero argument and auto-evaluates itself upon observation (i.e. no empty parenthesis).
a, b, c = 1, 2, `a + b`
print(c) # 3
a, b = 3, 4
print(c) # 7
3. A non-backdoor approach for typing forward references
type X = `A | B`
class A: ...
class B: ...
Previous Attempts
PEP671 proposed a new operator => that works identical in use case 1. Unfortunately, this proposal have received too many objections and is currently stalled (thanks @Rosuav for sharing the information!).
A working prototype of defer keyword has been proposed in related topics 2 (see below). The defer keyword works analogous to the proposed back quotes. But it (1) takes a longer form and (2) may cause precedence problems for operators and separators (I might be wrong on this one).
My own proposal of “deferred arg list” - this does not raise much interest in the community. And the keyword decorator I proposed lacks flexibility and will cause confusions on its behavior.
functools.lazy and other 3rd party wheels for lazy evaluation (thanks @dg-pb for brining them up!) - they covers most of the cases above. But lack of interpreter support exposed some operations that cannot be properly proxied (such as is and type). In addition, lack of syntax sugar makes them cumbersome to spell:
# Proposed
def task1(next_task = `task2`): ...
# Existing: functools.lazy
def task1(next_task = functools.lazy(lambda: task2)): ...
# Shortest possible form: from functools import lazy as L
# Not friendly to code reviewers or readers
def task1(next_task = L(lambda: task2)): ...
Implementation Details:
Collapsible?
It remains to be discussed if the DeferExpr variable should collapse upon its first evaluation. (i.e. replace itself with the evaluation result). This might be favorable for performance considerations. However, it will invalidate the first two use cases presented above.
IMO the answer would be not to collapse. In some corner cases where dynamic values causes problems user can explicitly collapse a deferred expr by stating x = x in their code. For functions that do not like dynamic values as their arguments, a syntax of fn(x:=x) will be sufficient.
Observation - namely the is and type keyword:
Option 1: logically distinguishable
x = `True`
type(x) # DeferExpr
x is x # DeferExpr(...) is DeferExpr(...)? => True
x is True # DeferExpr(...) is True? => False
x.__expr__ # "True"
x.__locals__ # locals() at where it is declared
x.__globals__ # globals() at the module that it is declared
Option 2: completely undistinguishable - secret to the interpreter
x = `object()`
type(x) # object (an observation of the value)
x is x # object() is object()? => False
# In case you REALLY need to tell...
inspect.is_defer_expr(x) # True
This remains to be discussed - and it’s hard to tell which will work better among all use cases.
Typing
A deferred expression needs to be typed differently than a normal value, e.g. DeferExpr[T]. It seamlessly fits into any slots that expects type T. However, as stated above, there should be a way to hint that DeferExpr is not allowed - and it needs to be discussed.
Related Contents:
PEP671 - Syntax for late-bound function argument defaults
functools.lazy - A discussion post bringing up proxy tools provided by both stdlib and 3rd party.
P.S.
I know this topic have been revisited many times. I myself stated in my original post “it might be too late to even bring this up for discussion”. However upon second thought I think this idea deserves to be presented to the community - even if it will very likely stall again.
No, but given Python’s convention of 79 character lines, I frequently need to wrap lines.
So, if the deferred expression is too long, it won’t fit on the line and my IDE will complain.
My current take on this is that convenience syntax decisions can be left for the very end.
The major issues of this direction is not in syntax, but in concept and implementation.
Same as with many other syntax affecting proposals, my take is that it is best to explore whether good enough solution can be devised via implementing and combining a set of tools that use existing capabilities.
And question “whether syntax conveniences are needed” will be much easier to answer once there is a clear idea of what the functionality exactly is and its usefulness.
Can you name the obstacles on the implementation side, or point me to a post on them?
The behavior of the backquote syntax has been mostly defined in the OP. I can even write you a python pre-processor that replaces the back quote syntax (back-qoute<expr>) into lazy + lambda (functools.lazy(lambda: <expr>)) and it will cover a subset of what’s supported by the new syntax.
The link in that thread doesn’t work for some reason.
I have briefly explored syntax there too (including backticks). Although I was also initially pro-backticks, but there was feedback of some non-trivial historical complications for such. Also, if multiline expressions are supported, then backticks are awkward and it would be much more pythonic to have a statement-like pythonic-indentation syntax. Such as:
local_var = 1
a = lazy local_var:
b = local_var + 1
return b
But regardless, this syntax could just be syntactic convenience for:
local_var = 1
def foo(lazy a):
b = a + 1
return b
a = functools.lazy(foo, (local_var,))
But not necessarily of course. Alternatively implementation could be designed with more weight on parser, but I just don’t think it is a good idea.
These are hard to give without at least 70% baked concept.
Also, read-up the thread after reading the document. By now it is quite clear that “late bound argument defaults” and “deferred evaluation” are orthogonal concepts and are best not to be mixed up.
Agreed. It would be good to see robust functools.lazy implementation first.
I have done some initial drafts in Python and think it is possible, but the complexity of implementing such in C is greater by the order of magnitude. Have a look at proxy objects in wrapt package. And higher level of robustness, consideration for various nuances, etc, would be needed when implementing this in CPython stdlib.
I need a bit more time to finish reading, there are a lot of contents and posts that you pointed to. I might comeback with more questions when I am finished. (Somehow I missed the thread completely when doing my research writing this post)
Form what I’ve seen, you have already done a ton of impressive work on this. Really appreciate that you brought them up here!
I am super interested. Unfortunately I do not have the expertise to do this alone. I would really appreciate if someone with enough knowledge can guide me through this and make it happen.
As I said, late-bound-defaults is not part of this.
def foo(a, b, c=`a+b`):
...
would not work, because a and b are not visible when c default is assigned. This is a separate concept as per PEP671.
A new approach of declaring zero-argument lambdas
This is a weak motivation as zero-argument lambdas can be done using lambda with no arguments. In other words, effort required for this is much greater than this benefit.
Or to expose its usefulness, it would be good to see real life applications of this that show the benefit.
A non-backdoor approach for typing forward references
I don’t use typing, but this one seems useful to me.
Personally, I am going in a slightly different direction than your proposal.
However, a possible common denominator is functools.lazy, which from my POV would be a good starting point.
And it is much easier sell than full proposal with new syntax.
And given it proves to be useful, combined with the fact that implementation exists and is functioning well, selling syntactic convenience would be much easier.
Good catch. I did not elaborate on this since it will open up too much complexity. I probably shouldn’t use it to sell this proposal at all.
I agree. Instead of a selling point, this section is more about how to “describe” it - a “mental model” to help explain the new syntax to people who knows python but does not know this syntax.
In addition, if this proposal is lucky enough to get a chance for prototyping, I bet it will reuse a lot of infrastructures made for lambda functions.
They weren’t really. They were listed in PEP 3099 as something that wasn’t going to be used. So you’ll have to explain what’s changed since then that means that they are now available for use.
But as with every proposal for magical deferred expressions, the key is semantics. Let’s look at yours:
Does it simply insert the text of the expression[1] at that point? What variable scopes are available to it?
Does it capture variables or doesn’t it? If it doesn’t, this is so completely different from a lambda function that it’s only going to cause confusion to talk about them in parallel. If it does, it won’t work for function defaults.
And the biggest problem with every “magical deferred” proposal: WHEN does it get evaluated? What are the semantics of this “deferred object”? For example, if I do this:
x = `a + b`
print(x)
func(x)
what happens? Does it evaluate more than once? Does the first one print out the DeferExpr but the second one evaluate it? Does it capture the values of a and b from the caller’s scope, or does it reach into the callee’s scope to fetch them? This is a HUGE area that you will need to explain in detail. The biggest downfall of every “deferred object” proposal I’ve seen has been the inability to precisely define these semantics.
This is why PEP 671 never said anything of the sort. There is no deferred object. It is a syntactic structure that looks at the lack of an argument and executes some code.
The expression is immediately parsed and should be converted to byte code just like any other code. Suppose you have syntax error inside it, then it should be raised during syntax evaluation time, not runtime.
The expression will capture the locals and globals at where it’s declared. It’s covered in the OP under Implementation Details 2.1.
This has been addressed in Implementation Details 1 (collapsible). I made it clear that it remains to be discussed, but favored no-collapsing and provided solutions for corner cases.