On Fri, Dec 3, 2021 at 9:26 AM Brendan Barnwell <brenb...@brenbarn.net> wrote:
>
> 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).

Ah, I think I get you. The problem is that code is in the def line but
is only executed when the function is called, is that correct? Because
the code would be "re-executed" just as much if it were written in the
function body. It's executed (at most) once for each call to the
function, just like the ternary's side is.

I suppose that's a consideration, but it's not nearly as strong in
practice as you might think. A lot of people aren't even aware of the
difference between compilation time and definition time (even people
on this list have made that mistake). Function default args are
executed when the function is defined, not when it's called, and
that's something that changes with this proposal; but there are many
other subtleties to execution order and timing that don't really
matter in practice.

Perhaps the key point here is to consider function decorators. We
could avoid them altogether:

def f():
    @deco
    def g(x): ...

    def h(x): ...
    h = deco(h)

But as well as having the name replication problem, this buries
important information down in the body of the surrounding code, rather
than putting it at the signature of g/h where it belongs. Even though,
semantically, this is actually part of the body of f, we want to be
able to read it as part of the signature of g. Logically and
conceptually, it is part of the signature. Now compare these two:

def f2():
    _SENTINEL = object()
    def g(x=_SENTINEL):
        if x is _SENTINEL: x = []
        ...

    def h(x=>[]):
        ...

Which one has its signature where its signature belongs? Yes,
semantically, the construction of the empty list happens at function
call time, not at definition time. But what you're saying is: if there
are no args passed, behave as if a new empty list was passed. That's
part of the signature.

In neither case will you find an object representing the expression []
in the function's signature, because that's not an object, it's an
instruction to build an empty list. In the case of g, you can find a
meaningless and useless object stashed away in __defaults__, but that
doesn't tell you anything about the true behaviour of the function. At
least in the case of h, you can find the descriptive string "[]"
stashed there, which can tell a human what's happening.

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

Reply via email to