This is slightly revised version of something I sent to Stefano two
weeks ago. I hope he is planning to use this, or something similar, in
the PEP, but for what it's worth here it is for discussion.
This is, as far as I can tell, the minimum language change needed to
support keywords in subscripts, and it will support all the desired
use-cases.
* * *
(1) An empty subscript is still illegal, regardless of context.
obj[] # SyntaxError
(2) A single subscript remains a single …
[View More]argument:
obj[index]
# calls type(obj).__getitem__(index)
obj[index] = value
# calls type(obj).__setitem__(index, value)
del obj[index]
# calls type(obj).__delitem__(index)
(This remains the case even if the index is followed by keywords; see
point 5 below.)
(3) Comma-seperated arguments are still parsed as a tuple and passed as
a single positional argument:
obj[spam, eggs]
# calls type(obj).__getitem__((spam, eggs))
obj[spam, eggs] = value
# calls type(obj).__setitem__((spam, eggs), value)
del obj[spam, eggs]
# calls type(obj).__delitem__((spam, eggs))
Points (1) to (3) mean that classes which do not want to support keyword
arguments in subscripts need do nothing at all. (Completely backwards
compatible.)
(4) Keyword arguments, if any, must follow positional arguments.
obj[1, 2, spam=None, 3) # SyntaxError
This is like function calls, where intermixing positional and keyword
arguments give a SyntaxError.
(5) Keyword subscripts, if any, will be handled like they are in
function calls. Examples:
# Single index with keywords:
obj[index, spam=1, eggs=2]
# calls type(obj).__getitem__(index, spam=1, eggs=2)
obj[index, spam=1, eggs=2] = value
# calls type(obj).__setitem__(index, value, spam=1, eggs=2)
del obj[index, spam=1, eggs=2]
# calls type(obj).__delitem__(index, spam=1, eggs=2)
# Comma-separated indices with keywords:
obj[foo, bar, spam=1, eggs=2]
# calls type(obj).__getitem__((foo, bar), spam=1, eggs=2)
and *mutatis mutandis* for the set and del cases.
(6) The same rules apply with respect to keyword subscripts as for
keywords in function calls:
- the interpeter matches up each keyword subscript to a named parameter
in the appropriate method;
- if a named parameter is used twice, that is an error;
- if there are any named parameters left over (without a value) when the
keywords are all used, they are assigned their default value (if any);
- if any such parameter doesn't have a default, that is an error;
- if there are any keyword subscripts remaining after all the named
parameters are filled, and the method has a `**kwargs` parameter,
they are bound to the `**kwargs` parameter as a dict;
- but if no `**kwargs` parameter is defined, it is an error.
(7) Sequence unpacking remains a syntax error inside subscripts:
obj[*items]
Reason: unpacking items would result it being immediately repacked into
a tuple. Anyone using sequence unpacking in the subscript is probably
confused as to what is happening, and it is best if they receive an
immediate syntax error with an informative error message.
(8) Dict unpacking is permitted:
items = {'spam': 1, 'eggs': 2}
obj[index, **items]
# equivalent to obj[index, spam=1, eggs=2]
(9) Keyword-only subscripts are permitted:
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)
but note that the setter is awkward since the signature requires the
first parameter:
obj[spam=1, eggs=2] = value
# wants to call type(obj).__setitem__(???, value, spam=1, eggs=2)
Proposed solution: this is a runtime error unless the setitem method
gives the first parameter a default, e.g.:
def __setitem__(self, index=None, value=None, **kwargs)
Note that the second parameter will always be present, nevertheless, to
satisfy the interpreter, it too will require a default value.
(Editorial comment: this is undoubtably an awkward and ugly corner case,
but I am reluctant to prohibit keyword-only assignment.)
Comments
--------
(a) Non-keyword subscripts are treated the same as the status quo,
giving full backwards compatibility.
(b) Technically, if a class defines their getter like this:
def __getitem__(self, index):
then the caller could call that using keyword syntax:
obj[index=1]
but this should be harmless with no behavioural difference. But classes
that wish to avoid this can define their parameters as positional-only:
def __getitem__(self, index, /):
(c) If the method is declared with no positional arguments (aside from
self), only keyword subscripts can be given:
def __getitem__(self, *, index)
# requires obj[index=1] not obj[1]
Although this is unavoidably awkward for setters:
# Intent is for the object to only support keyword subscripts.
def __setitem__(self, i=None, value=None, /, *, index)
if i is not None:
raise TypeError('only keyword arguments permitted')
Gotchas
-------
If the subscript dunders are declared to use positional-or-keyword
parameters, there may be some surprising cases when arguments are passed
to the method. Given the signature:
def __getitem__(self, index, direction='north')
if the caller uses this:
obj[0, 'south']
they will probably be surprised by the method call:
# expected type(obj).__getitem__(0, direction='south')
# but actually get:
obj.__getitem__((0, 'south'), direction='north')
Solution: best practice suggests that keyword subscripts should be
flagged as keyword-only when possible:
def __getitem__(self, index, *, direction='north')
The interpreter need not enforce this rule, as there could be scenarios
where this is the desired behaviour. But linters may choose to warn
about subscript methods which don't use the keyword-only flag.
--
Steve
[View Less]
For those interested in the new draft of ex PEP-0472 (which has joined
the choir invisible), I opened a draft PR
https://github.com/python/peps/pull/1579
This first draft contains only the revisited motivation and
background. I'll add the technical decisions etc in the upcoming
nights.
--
Kind regards,
Stefano Borini