Revisit Mutable Default Arguments

…or not.

A new assignment operator would not help with the case of:

return a, defer complex_expression

I apologize for not dropping this subject. It’s because my intuition tells me that we’re an epiphany away from a reasonable solution.

Part of the problem to solve is the semantics of deferred values so they are consisten and not a special case in argument declarations.

This is odd in Python, but I think it’s the semantics we want:

>>> x = defer []
>>> y = x
>>> y is x
False
>>> y == x
True

Those semantics are manageable, but the syntax begs for the meaning of:

>>> x = defer [] + [7]

The above calls for the special binding operator that @Rosuav suggested:

>>> x => defer [] +[7]
>>> y = x
>>> y is x
False
>>> y == x
True
>>> y == [7]
True

Which is probably because your intuition is identical to everyone else’s, seeking the obvious but impossible solution. Remember that, for default arguments, the semantics are as follows:

_DEFAULT_VALUE = some expression
def func(x):
    if x was omitted: x = _DEFAULT_VALUE

But late-bound argument defaults are as follows:

def func(x):
    if x was omitted: x = some expression

Try to create your deferred expressions such that they will re-evaluate every time the function is called, but not multiple times within that. For example, the classic empty list situation:

def add(x=defer []):
    x.append(1)
    x.append(2)
    return x

This needs to return a new list every time it’s called without args. What are your semantics and how do you make this work? Every time, it comes back to magic: “just do what I want”.

1 Like

But @Rosuav did not suggest a special binding operator. He suggested a syntax for late binding valid only in function definitions.

1 Like

Having a syntax only for function definitions will break language symetry. But there’s already plenty of syntax that applies only to function definitions, so… :person_shrugging:

I’d argue that is not a case, because the x in the definition is not the same as the x in the function body.

The semantics would be:

definition_x = defer []
x = definition_x

I think @Rosuav’s PEP 671 does not need that many changes to solve this for dataclasses as well:

For use cases where field isn’t required, it can work like this (Unless rthis is not desired for a reason I’m missing)

@dataclass
class A:
    b: list => []

If you’re already using field, your dataclass definition is going to be verbose anyway, so I think it’s fine staying with a default_factory:

@dataclass
class C:
    d: list = field(default_factory=list)

But if you think this is not an elegant enough solution, then you should also agree this is also not very elegant with early-bound defaults as well:

@dataclass
class E:
    f: int= field(default=10)

IMO if early-bound defaults don’t solve this, then late-bound defaults don’t have to either.

If you do however want to solve this, dataclasses could take the Pydantic approach and use Annotated (Dataclasses - make use of Annotated):

@dataclass
class G:
    h: Annotated[int, field(...)] = 10

@dataclass
class I:
    j: Annotated[list, field(...)] => []

Correct, although whether it’s called an “operator” or something else is merely taxonomical. But yes, the late binding I propose is a feature of function definitions and calls, not an operator that can be used elsewhere.

1 Like

I’m lost. The x in the definition is a function parameter, and the one in the body is using that parameter, is it not? How is it not the same?

If done with PEP 671 syntax and semantics, it would look like this:

def add(x=>[]):
    x.append(1)
    x.append(2)
    return x

and every time you call it with no args, it would return you a brand new list with two elements in it.

So if your intuition is telling you that there’s a reasonable solution waiting, go for it. Find one. Create an actual solid proposal. I genuinely believe that a deferred evaluation proposal has the potential to be very useful; it’s just that I haven’t yet heard from anyone who’s gone to the work of actually defining detailed semantics, much less writing up a PEP. But unless you are willing to push this forward, will you PLEASE acknowledge that we have been around this merry-go-round a number of times already, and “generic deferreds” are NOT an implementation for late-bound argument defaults?

The one thing that I really do not understand here is why people think that I, as the author of PEP 671, am obligated to invent the perfect syntax and semantics for deferred expressions. They are not the same proposal.

4 Likes

Well, it’s a syntax for function parameters, so it’s kinda specific to function definitions :slight_smile:

There are two execution contexts:

  1. On function declaration the deferred value is assigned as the argument’s default, and the name is kept for future matching of keyword arguments.
  2. On function call, space is allocated on the stack for the argument which is set to the actual passed value or assigned the default. In the case of a deferred default, that latter assignment copies the value.

As to the semantics, it would break havoc if the expression in => expre was kept for later evaluation, because that would mean keeping the context in which the expression makes sense. To resolve the default argument case the expression must be evaluated to a value that is copied on each function call.

That said, it seems that the semantics of these “deferred” expressions are not interesting outside of function declarations, which in turn means that “deferred” may not be the best name when what is needed is a copy of a value.

What I mean is that the value of the expression is resolved at function declaration:

def f(x=>g(a)):  # a value, not a function call
   ...

The next step should be to look at the code for actual argument resolution to find a place in which it’s convenient to check some flag and perform a copy of the value. After having resolved positional-only up to keyword-only having a special kind of keyword shouldn’t be that hard.

@Rosuav I read PEP 671 again, and I think it will work with these modifications:

  1. No late evaluation. Argument defaults remain as values.
  2. Simplification of the implementation. All that’s needed is a flag the orders the resolver to copy the value.

I suggest you write a revised implementation (and probably an updated PEP as well). Clearly @Rosuav disagrees with you, so it’s time to demonstrate your suggestion with working code not words. Otherwise this is going nowhere, so if you’re not willing to write the implementation you propose, I suggest you drop the discussion.

3 Likes

Well, that’s a completely different proposal. Fortunately, if this is all you want, you can create a decorator that does it. This saves you the trouble of writing up a competing PEP, but once again, we have someone who’s objecting to a proposal by wondering why it isn’t a completely different one.

Without enough interest and defined semantics it doesn’t make sense to write any code.

Besides, though I know exactly what needs be done (probably a few lines of code), learning to work with the C in CPython is well beyond the scope of the work I can put into this.

Then demonstrate your suggestion with defined semantics. It is a completely separate suggestion to mine, so ignore mine and start by laying out what yours will do, how it would benefit people, and preferably, some examples (maybe from the stdlib) of how this could be used to simplify existing code.

A decorator might be the epiphany that produces a reasonable solution now.

The problem with a decorator is that it receives argument values already resolved, so it doesn’t have a way to know if a [] came from the declaration or the caller. Also there need to be provisions so typing annotations are simple.

There would need to be a helper to wrap arguments of interest, and that lies about the returned type:

T = TypeVar("T")

class _deferred(NamedTuple):
   actual: Any

def defer(o: T) -> T: 
   return _deferred(actual=o) # type: T
   ...

@defaultargs
def f(x: list[Any] = defer([])):
   ...

If the above works, the semantics should be easy to implement within the interpreter.

Start a thread. Write up a proposal. You have all the tools you need.

The idea is something more fitting for Github and PyPI.

Thanks for all your ideas and patience, @Rosuav!

The above semantics are implemented here:

It’s up to decission makers if something like that ever gets implemented as part of the interpreter.