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 -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/SGVCDKUSZYIHHJQY3CGRTZ4FNRD2WOAK/
Code of Conduct: http://python.org/psf/codeofconduct/