Change of subject line as I wish to focus on a single critical point of 
the PEP: keyword-only subscripts.

TL;DR:

1. We have to pass a sentinel to the setitem dunder if there is no 
positional index passed. What should that sentinel be?


*  None
*  the empty tuple ()
*  NotImplemented
*  something else


2. Even though we don't have to pass the same sentinel to getitem and 
delitem dunders, we could. Should we?


*  No, getitem and delitem should use no sentinel.
*  Yes, all three dunders should use the same rules.
*  Just prohibit keyword-only subscripts.


(Voting is non-binding. It's feedback, not a democracy :-)

Please read the details below before voting. Comments welcome.


----------------------------------------------------------------------


For all three dunders, there is no difficulty in retrofitting keyword 
subscripts to the dunder signature if there is there is a positional 
index:

    obj[index, spam=1, eggs=2]
    # => calls type(obj).__getitem__(index, spam=1, eggs=2)

    del obj[index, spam=1, eggs=2]
    # => calls type(obj).__delitem__(index, spam=1, eggs=2)

    obj[index, spam=1, eggs=2] = value
    # => calls type(obj).__setitem__(index, value, spam=1, eggs=2)


If there is no positional index, the getitem and delitem calls are 
easy:

    obj[spam=1, eggs=2]
    # => calls type(obj).__getitem__(spam=1, eggs=2)

    del obj[spam=1, eggs=2]
    # => calls type(obj).__delitem__(spam=1, eggs=2)

If the dunders are defined with a default value for the index, the call 
will succeed; if there is no default, you will get a TypeError. This is 
what we expect to happen.

But setitem is hard:


    obj[spam=1, eggs=2] = value
    # => calls type(obj).__setitem__(???, value, spam=1, eggs=2)


Python doesn't easily give us a way to call a method and skip over 
positional arguments. So it seems that setitem needs to fill in a fake 
placeholder. Three obvious choices are None, the empty tuple () or 
NotImplemented.

All three are hashable, so they could be used as legitimate keys in a 
mapping; but in practice, I expect that only None and () would be. I 
can't see very many objects actually using NotImplemented as a key.

numpy also uses None to force creation of a new axis. I don't think that 
*quite* rules out None: numpy could distinguish the meaning of None as a 
subscript depending on whether or not there are keyword args.

But NotImplemented is special:

- I don't expect anyone is using NotImplemented as a key or index.

- NotImplemented is already used a sentinel for operators to say "I 
don't know how to handle this"; it's not far from that to interpret it 
as saying "I don't know what value to put in this positional argument".

- Starting in Python 3.9 or 3.10, NotImplemented is even more special: 
it no longer ducktypes as truthy or falsey. This will encourage people 
to explicitly check for it:

    if index is NotImplemented: ...

rather than `if index: ...`.

So I think that NotImplemented is a better choice than None or an empty 
tuple.

Whatever sentinel we use, that implies that setitem cannot distingish 
these two cases:


    obj[SENTINEL, spam=1, eggs=2] = value
    obj[spam=1, eggs=2] = value


Since both None and () are likely to be legitimate indexes, and 
NotImplemented is less likely to be such, I think this supports using 
NotImplemented.

But whichever sentinel we choose, that brings us to the second part of 
the problem.

What should getitem and delitem do?

setitem must provide a sentinel for the first positional argument, but 
getitem and delitem don't have to. So we could have this:


    # Option 1: only setitem is passed a sentinel

    obj[spam=1, eggs=2]
    # => calls type(obj).__getitem__(spam=1, eggs=2)

    del obj[spam=1, eggs=2]
    # => calls type(obj).__delitem__(spam=1, eggs=2)

    obj[spam=1, eggs=2] = value
    # => calls type(obj).__setitem__(SENTINEL, value, spam=1, eggs=2)


Advantages:

- The simple getitem and delitem cases stay simple; it is only the
  complicated setitem case that is complicated.

- getitem and delitem can distinguish the "no positional index at all" 
  case from the case where the caller explicitly passes the sentinel
  as a positional index; only setitem cannot distinguish them. If your
  class doesn't support setitem, this might be useful to you.

Disadvantages:

- Inconsistency: the rules for one dunder are different from the other
  two dunders.

- If your class does distinguish between no positional index, and the 
  sentinel, that means that there is a case that getitem and delitem can 
  handle but setitem cannot.


Or we could go with an alternative:

    # Option 2: all three dunders are passed a sentinel

    obj[spam=1, eggs=2]
    # => calls type(obj).__getitem__(SENTINEL, spam=1, eggs=2)

    del obj[spam=1, eggs=2]
    # => calls type(obj).__delitem__(SENTINEL, spam=1, eggs=2)

    obj[spam=1, eggs=2] = value
    # => calls type(obj).__setitem__(SENTINEL, value, spam=1, eggs=2)


Even though the getitem and delitem cases don't need the sentinel, they 
get them anyway.

This has the advantage that all three dunders are treated the same, and 
that there is no case that two of the dunders will handle but the third 
does not.

But it also means that subscript dunders cannot meaningfully provide 
their own default for the index in the function signature:

    def __getitem__(self, index=0, *, spam, eggs)

will always receive a value for index, not the default. So we need to 
check that inside the body:

    if index is SENTINEL:
        index = 0

There's a third option: just prohibit keyword-only subscripts. I think 
that's harsh, throwing the baby out with the bathwater. I personally 
have use-cases where I would use keyword-only subscripts so I would 
prefer options 1 or 2.



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

Reply via email to