On Mon, 25 Oct 2021, Chris Angelico wrote:

On Mon, Oct 25, 2021 at 6:13 PM Steven D'Aprano <st...@pearwood.info> wrote:

The rules for applying parameter defaults are well-defined. I would have
to look it up to be sure...

And that right there is all the evidence I need. If you, an
experienced Python programmer, can be unsure, then there's a strong
indication that novice programmers will have far more trouble. Why
permit bad code at the price of hard-to-explain complexity?

I'm not sure how this helps; the rules are already a bit complicated. Steven's proposed rules are a natural way to extend the existing rules; I don't see the new rules as (much) more complicated.

Offer me a real use-case where this would matter. So far, we had
better use-cases for arbitrary assignment expression targets than for
back-to-front argument default references, and those were excluded.

I can think of a few examples, though they are a bit artificial:

```
def search_listdir(path = None, files := os.listdir(path),
                   start = 0, end = len(files)):
    '''specify path or files'''

# variation of the LocaleTextCalendar from stdlib (in a message of Steven's)
class Calendar:
    default_firstweekday = 0
    def __init__(self, firstweekday := Calendar.default_firstweekday,
                 locale := find_default_locale(),
                 firstweekdayname := locale.lookup_day_name(firstweekday)):
        ...
Calendar.default_firstweekday = 1
```

But I think the main advantage of the left-to-right semantics is simplicity and predictability. I don't think the following functions should evaluate the default values in different orders.

```
def f(a := side_effect1(), b := side_effect2()): ...
def g(a := side_effect1(), b := side_effect2() + a): ...
def h(a := side_effect1() + b, b := side_effect2()): ...
```

I expect left-to-right semantics of the side effects (so function h will probably raise an error), just like I get from the corresponding tuple expressions:

```
(a := side_effect1(), b := side_effect2())
(a := side_effect1(), b := side_effect2() + a)
(a := side_effect1() + b, b := side_effect2())
```

As Jonathan Fine mentioned, if you defined the order to be a linearization of the partial order on arguments, (a) this would be complicated and (b) it would be ambiguous. I think, if you're going to forbid `def f(a := b, b:= a)` at the compiler level, you would need to forbid using late-bound arguments (at least) in least-bound argument expressions. But I don't see a reason to forbid this. It's rare that order would matter, and if it did, a quick experiment or learning "left to right" is really easy.

The tuple expression equivalence leads me to think that `:=` is decent notation. As a result, I would expect:

```
def f(a := expr1, b := expr2, c := expr3): pass
```

to behave the same as:

```
_no_a = object()
_no_b = object()
_no_c = object()
def f(a = _no_a, b = _no_b, c = _no_c):
    (a := expr1 if a is _no_a else a,
     b := expr2 if b is _no_b else b,
     c := expr3 if c is _no_c else c)
```

Given that `=` assignments within a function's parameter spec already only means "assign when another value isn't specified", this is pretty similar.

On Mon, 25 Oct 2021, Chris Angelico wrote:

On Sun, 24 Oct 2021, Erik Demaine wrote:

> I think the semantics are easy to specify: the argument defaults get > evaluated for unspecified ARGUMENT(s), in left to right order as specified > in the def. Those may trigger exceptions as usual.

Ah, but is it ALL argument defaults, or only those that are
late-evaluated? Either way, it's going to be inconsistent with itself
and harder to explain. That's what led me to change my mind.

I admit I missed this subtlety, though again I don't think it would often make a difference. But working out subtleties is what PEPs and discussion are for. :-)

I'd be inclined to assign the early-bound argument defaults before the late-bound arguments, because their values are already known (they're stored right in the function argument) so they can't cause side effects, and it could offer slight incremental benefits, like being able to write the following (again, somewhat artificial):

```
def manipulate(top_list):
    def recurse(start=0, end := len(rec_list), rec_list=top_list): ...
```

But I don't feel strongly either way about either interpretation.

Mixing both types of default arguments breaks the analogy to tuple expressions above, alas. The corresponding tuple expression with `=` is just invalid.

Personally, I'd expect to use late-bound defaults almost all or all the time; they behave more how I expect and how I usually need them (I use a fair amount of `[]` and `{}` and `set()` as default values). The only context I'd use/want the current default behavior is to hack closures, as in:

```
for thing in things:
    thing.callback = lambda thing=thing: print(thing.name)
```

I believe the general preference for late-bound defaults is why Guido called this a "wart" in https://mail.python.org/archives/list/python-ideas@python.org/message/T4VPHDZJUP4QYIXJA26JQ6EQJJKRUEYP/


By the way, JavaScript's semantics for default arguments are just like what I'm describing: they are evaluated at call time, in the function scope, and in left-to-right order.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#earlier_parameters_are_available_to_later_default_parameters
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#earlier_parameters_are_available_to_later_default_parameters

A key difference from the PEP is that JavaScript doesn't have the notion of "omitted arguments"; any omitted arguments are just passed in as `undefined`; so `f()` and `f(undefined)` always behave the same (triggering default argument behavior).

There is a subtlety mentioned in the case of JavaScript, which is that the default value expressions are evaluated in their own scope:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#scope_effects

This is perhaps worth considering for the Python context. I'm not sure this is as important in Python, because UnboundLocalError exists (so attempts to access things in the function's scope will fail), but perhaps I'm missing a ramification...

Erik
--
Erik Demaine  |  edema...@mit.edu  |  http://erikdemaine.org/
_______________________________________________
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/CFDCHAMTJL2HQJKYJKGX6O3A6KDGJDJC/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to