On 2021-12-02 00:31, Chris Angelico wrote:
Here's how a ternary if looks:
>def f(n): ... return 0 if n == 0 else 42/n ... >dis.dis(f) 2 0 LOAD_FAST 0 (n) 2 LOAD_CONST 1 (0) 4 COMPARE_OP 2 (==) 6 POP_JUMP_IF_FALSE 6 (to 12) 8 LOAD_CONST 1 (0) 10 RETURN_VALUE >> 12 LOAD_CONST 2 (42) 14 LOAD_FAST 0 (n) 16 BINARY_TRUE_DIVIDE 18 RETURN_VALUE
The "42/n" part is stored in f.__code__.co_code as the part that says "LOAD_CONST 42, LOAD_FAST n, BINARY_TRUE_DIVIDE". It's not an object. It's just code - three instructions.
Here's how (in the reference implementation - everything is subject to change) a late-bound default looks:
>def f(x=>[]): print(x) ... >dis.dis(f) 1 0 QUERY_FAST 0 (x) 2 POP_JUMP_IF_TRUE 4 (to 8) 4 BUILD_LIST 0 6 STORE_FAST 0 (x) >> 8 LOAD_GLOBAL 0 (print) 10 LOAD_FAST 0 (x) 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 0 (None) 18 RETURN_VALUE
The "=>[]" part is stored in f.__code__.co_code as the part that says "QUERY_FAST x, and if false, BUILD_LIST, STORE_FAST x". It's not an object. It's four instructions in the bytecode.
In both cases, no part of the expression is ever re-executed. I'm not understanding the distinction here. Can you explain further please?
Your explanation exactly shows how it IS re-executed. I'm not totally clear on this disassembly since this is new behavior, but if I understand right, BUILD_LIST is re-executing the expression `[]` and STORE_FAST is re-assigning it to x. The expression `[]` is syntactically present in the function definition but its execution has been shoved into the function body where it may be re-executed many times (any time the function is called without passing a value). What do you mean when you say it is not re-executed? Is it not the case that `[]` is syntactically present in the `def` line (which is executed only once, and whose bytecode is not shown) yet its implementation (BUILD_LIST) is in the function body, which may be executed many times? How is the BUILD_LIST opcode there not being re-executed on later calls of the function? Perhaps what you are saying is that what is stored is not the literal string "[]" but bytecode that implements it? That distinction is meaningless to me. The point is that you wrote `[]` once, in a line that is executed once (the function definition itself), but the `[]` is executed many times, separately from the function definition. Another way to put it is, again, the examples are not parallel. In your first example the ternary expression is (syntactically) part of the function BODY, so of course it appears in the disassembly. In your second example, the late-bound default is not in the body, it is in the signature. The disassembly is only showing the bytecode of the function body, but the late-bound default is syntactically enmeshed with the function DEFINITION. So I don't want the late-bound default code to be re-executed unless the function is re-defined. Or, if you're going to store that `x = []`, I don't want it to be "stored" by just shoving it in the function body, I want it to be some kind of separate object. Here's an example that may make my point clearer. some_function(x+1, lambda x: x+2) This is our best current approximation to some kind of "late evaluation". The first argument, `x+1`, is evaluated before the function is called, and the function gets only the result. The second argument is also of course evaluated before the function is called, but what is evaluated is a lambda; the `x+2` is not evaluated. But the idea of "evaluate x+2 later" is encapsulated in a function object. Without knowing anything about `some_function`, I still know that there is no way it can ever evaluate `x+2` without going through the interface of that function object. There is no way to pass, as the second argument to `some_function` some kind of amorphous "directive" that says "evaluate x+2 later" without wrapping that directive up into some kind of Python object. Perhaps the fundamental point that I feel you're missing about my position is that a ternary expression does not have a "definition" and a "body" that are executed at separate times. There is just the whole ternary expression and it is evaluated all at once. Thus there can be no parallel with functions, which do have a separation between definition time and call time. Indeed, it's because these are separate that late vs. early binding of defaults is even a meaningful concept for functions (but not for ternary expressions). So what I am saying is if I see a line like this: def f(a=x+1, b@=x+2): The x+2 is syntactically embedded within the `def` line, that is, the function definition (not the body). Thus there are only two kinds of semantics that are going to make me happy: 1) That `x+2`(or any bytecode derived from it, etc.) will never be re-executed unless the program execution again reaches the line with the `def f` (which is what we have with early-bound defaults) 2) A Python object is created that encapsulates the expression `x+2` somehow and defines what it means to "evaluate it in a context" (e.g., by referencing local variables in the scope where it is evaluated) Maybe another way to say this is just "I want any kind of late-bound default to really be an early-bound default whose value is some object that provides a way to evaluate it later". (I'm trying to think of different ways to say this because it seems what I'm saying is not clear to you. :-) -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown