PEP 637: Renamed file to rst and changed formatting of block code (#1619)

This commit is contained in:
Stefano Borini 2020-09-26 01:00:29 +01:00 committed by GitHub
parent ef4663634e
commit 8e16d32598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 241 additions and 383 deletions

View File

@ -20,21 +20,16 @@ At present keyword arguments are allowed in function calls, but not in
item access. This PEP proposes that Python be extended to allow keyword item access. This PEP proposes that Python be extended to allow keyword
arguments in item access. arguments in item access.
The following example shows keyword arguments for ordinary function calls: The following example shows keyword arguments for ordinary function calls::
:: >>> val = f(1, 2, a=3, b=4)
>>> val = f(1, 2, a=3, b=4)
The proposal would extend the syntax to allow a similar construct The proposal would extend the syntax to allow a similar construct
to indexing operations: to indexing operations::
::
>>> val = x[1, 2, a=3, b=4] # getitem
>>> x[1, 2, a=3, b=4] = val # setitem
>>> del x[1, 2, a=3, b=4] # delitem
>>> val = x[1, 2, a=3, b=4] # getitem
>>> x[1, 2, a=3, b=4] = val # setitem
>>> del x[1, 2, a=3, b=4] # delitem
and would also provide appropriate semantics. and would also provide appropriate semantics.
@ -79,66 +74,50 @@ The following practical use cases present different cases where a keyworded
specification would improve notation and provide additional value: specification would improve notation and provide additional value:
1. To provide a more communicative meaning to the index, preventing e.g. accidental 1. To provide a more communicative meaning to the index, preventing e.g. accidental
inversion of indexes inversion of indexes::
:: >>> grid_position[x=3, y=5, z=8]
>>> rain_amount[time=0:12, location=location]
>>> matrix[row=20, col=40]
>>> grid_position[x=3, y=5, z=8] 2. To enrich the typing notation with keywords, especially during the use of generics::
>>> rain_amount[time=0:12, location=location]
>>> matrix[row=20, col=40]
2. To enrich the typing notation with keywords, especially during the use of generics
::
def function(value: MyType[T=int]):
def function(value: MyType[T=int]):
3. In some domain, such as computational physics and chemistry, the use of a 3. In some domain, such as computational physics and chemistry, the use of a
notation such as ``Basis[Z=5]`` is a Domain Specific Language notation to represent notation such as ``Basis[Z=5]`` is a Domain Specific Language notation to represent
a level of accuracy a level of accuracy::
:: >>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
>>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3]) 4. Pandas currently uses a notation such as::
4. Pandas currently uses a notation such as >>> df[df['x'] == 1]
:: which could be replaced with ``df[x=1]``.
>>> df[df['x'] == 1] 5. xarray has named dimensions. Currently these are handled with functions .isel::
which could be replaced with df[x=1]. >>> data.isel(row=10) # Returns the tenth row
5. xarray has named dimensions. Currently these are handled with functions .isel: which could also be replaced with ``data[row=10]``. A more complex example::
:: >>> # old syntax
>>> da.isel(space=0, time=slice(None, 2))[...] = spam
>>> # new syntax
>>> da[space=0, time=:2] = spam
>>> data.isel(row=10) # Returns the tenth row Another example::
which could also be replaced with `data[row=10]`. A more complex example: >>> # old syntax
>>> ds["empty"].loc[dict(lon=5, lat=6)] = 10
:: >>> # new syntax
>>> ds["empty"][lon=5, lat=6] = 10
>>> # old syntax
>>> da.isel(space=0, time=slice(None, 2))[...] = spam >>> # old syntax
>>> # new syntax >>> ds["empty"].loc[dict(lon=slice(1, 5), lat=slice(3, None))] = 10
>>> da[space=0, time=:2] = spam >>> # new syntax
>>> ds["empty"][lon=1:5, lat=6:] = 10
Another example:
::
>>> # old syntax
>>> ds["empty"].loc[dict(lon=5, lat=6)] = 10
>>> # new syntax
>>> ds["empty"][lon=5, lat=6] = 10
>>> # old syntax
>>> ds["empty"].loc[dict(lon=slice(1, 5), lat=slice(3, None))] = 10
>>> # new syntax
>>> ds["empty"][lon=1:5, lat=6:] = 10
It is important to note that how the notation is interpreted is up to the It is important to note that how the notation is interpreted is up to the
implementation. This PEP only defines and dictates the behavior of python implementation. This PEP only defines and dictates the behavior of python
@ -159,13 +138,11 @@ Subscripting ``obj[x]`` is, effectively, an alternate and specialised form of
function call syntax with a number of differences and restrictions compared to function call syntax with a number of differences and restrictions compared to
``obj(x)``. The current python syntax focuses exclusively on position to express ``obj(x)``. The current python syntax focuses exclusively on position to express
the index, and also contains syntactic sugar to refer to non-punctiform the index, and also contains syntactic sugar to refer to non-punctiform
selection (slices). Some common examples: selection (slices). Some common examples::
:: >>> a[3] # returns the fourth element of 'a'
>>> a[1:10:2] # slice notation (extract a non-trivial data subset)
>>> a[3] # returns the fourth element of 'a' >>> a[3, 2] # multiple indexes (for multidimensional arrays)
>>> a[1:10:2] # slice notation (extract a non-trivial data subset)
>>> a[3, 2] # multiple indexes (for multidimensional arrays)
This translates into a ``__(get|set|del)item__`` dunder call which is passed a single This translates into a ``__(get|set|del)item__`` dunder call which is passed a single
parameter containing the index (for ``__getitem__`` and ``__delitem__``) or two parameters parameter containing the index (for ``__getitem__`` and ``__delitem__``) or two parameters
@ -185,19 +162,15 @@ violate this intrinsic meaning.
The second difference of the indexing notation compared to a function The second difference of the indexing notation compared to a function
is that indexing can be used for both getting and setting operations. is that indexing can be used for both getting and setting operations.
In python, a function cannot be on the left hand side of an assignment. In In python, a function cannot be on the left hand side of an assignment. In
other words, both of these are valid other words, both of these are valid::
:: >>> x = a[1, 2]
>>> a[1, 2] = 5
>>> x = a[1, 2] but only the first one of these is valid::
>>> a[1, 2] = 5
but only the first one of these is valid >>> x = f(1, 2)
>>> f(1, 2) = 5 # invalid
::
>>> x = f(1, 2)
>>> f(1, 2) = 5 # invalid
This asymmetry is important, and makes one understand that there is a natural This asymmetry is important, and makes one understand that there is a natural
imbalance between the two forms. It is therefore not a given that the two imbalance between the two forms. It is therefore not a given that the two
@ -208,61 +181,48 @@ arguments, unless the passed parameters are captured with \*args, in which case
they end up as entries in the args tuple. In other words, functions already they end up as entries in the args tuple. In other words, functions already
have anonymous argument semantic, exactly like the indexing operation. However, have anonymous argument semantic, exactly like the indexing operation. However,
__(get|set|del)item__ is not always receiving a tuple as the ``index`` argument __(get|set|del)item__ is not always receiving a tuple as the ``index`` argument
(to be uniform in behavior with \*args). In fact, given a trivial class: (to be uniform in behavior with \*args). In fact, given a trivial class::
class X:
:: def __getitem__(self, index):
print(index)
class X:
def __getitem__(self, index):
print(index)
The index operation basically forwards the content of the square brackets "as is" The index operation basically forwards the content of the square brackets "as is"
in the ``index`` argument: in the ``index`` argument::
:: >>> x=X()
>>> x[0]
>>> x=X() 0
>>> x[0] >>> x[0, 1]
0 (0, 1)
>>> x[0, 1] >>> x[(0, 1)]
(0, 1) (0, 1)
>>> x[(0, 1)] >>>
(0, 1) >>> x[()]
>>> ()
>>> x[()] >>> x[{1, 2, 3}]
() {1, 2, 3}
>>> x[{1, 2, 3}] >>> x["hello"]
{1, 2, 3} hello
>>> x["hello"] >>> x["hello", "hi"]
hello ('hello', 'hi')
>>> x["hello", "hi"]
('hello', 'hi')
The fourth difference is that the indexing operation knows how to convert The fourth difference is that the indexing operation knows how to convert
colon notations to slices, thanks to support from the parser. This is valid colon notations to slices, thanks to support from the parser. This is valid::
:: a[1:3]
a[1:3] this one isn't::
this one isn't f(1:3)
:: The fifth difference is that there's no zero-argument form. This is valid::
f(1:3) f()
The fifth difference is that there's no zero-argument form. This is valid this one isn't::
:: a[]
f()
this one isn't
::
a[]
New Proposal New Proposal
@ -296,9 +256,7 @@ operation, may be used to take indexing decisions to obtain the final index, and
will have to accept values that are unconventional for functions. See for will have to accept values that are unconventional for functions. See for
example use case 1, where a slice is accepted. example use case 1, where a slice is accepted.
The new notation will make all of the following valid notation: The new notation will make all of the following valid notation::
::
>>> a[1] # Current case, single index >>> a[1] # Current case, single index
>>> a[1, 2] # Current case, multiple indexes >>> a[1, 2] # Current case, multiple indexes
@ -308,9 +266,7 @@ The new notation will make all of the following valid notation:
>>> a[3, R=3:10, K=4] # New case. Slice in keyword argument >>> a[3, R=3:10, K=4] # New case. Slice in keyword argument
>>> a[3, R=..., K=4] # New case. Ellipsis in keyword argument >>> a[3, R=..., K=4] # New case. Ellipsis in keyword argument
The new notation will NOT make the following valid notation: The new notation will NOT make the following valid notation::
::
>>> a[] # INVALID. No index and no keyword arguments. >>> a[] # INVALID. No index and no keyword arguments.
@ -326,81 +282,70 @@ Syntax and Semantics
The following old semantics are preserved: The following old semantics are preserved:
1. As said above, an empty subscript is still illegal, regardless of context. 1. As said above, an empty subscript is still illegal, regardless of context::
:: obj[] # SyntaxError
obj[] # SyntaxError 2. A single index value remains a single index value when passed::
2. A single index value remains a single index value when passed: obj[index]
# calls type(obj).__getitem__(obj, index)
obj[index] = value
# calls type(obj).__setitem__(obj, index, value)
del obj[index]
# calls type(obj).__delitem__(obj, index)
:: This remains the case even if the index is followed by keywords; see point 5 below.
obj[index]
# calls type(obj).__getitem__(obj, index)
obj[index] = value
# calls type(obj).__setitem__(obj, index, value)
del obj[index]
# calls type(obj).__delitem__(obj, 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 3. Comma-seperated arguments are still parsed as a tuple and passed as
a single positional argument: a single positional argument::
::
obj[spam, eggs]
# calls type(obj).__getitem__(obj, (spam, eggs))
obj[spam, eggs] = value
# calls type(obj).__setitem__(obj, (spam, eggs), value)
del obj[spam, eggs]
# calls type(obj).__delitem__(obj, (spam, eggs))
obj[spam, eggs]
# calls type(obj).__getitem__(obj, (spam, eggs))
obj[spam, eggs] = value
# calls type(obj).__setitem__(obj, (spam, eggs), value)
del obj[spam, eggs]
# calls type(obj).__delitem__(obj, (spam, eggs))
The points above mean that classes which do not want to support keyword The points above mean that classes which do not want to support keyword
arguments in subscripts need do nothing at all, and the feature is therefore arguments in subscripts need do nothing at all, and the feature is therefore
completely backwards compatible. completely backwards compatible.
4. Keyword arguments, if any, must follow positional arguments. 4. Keyword arguments, if any, must follow positional arguments::
:: obj[1, 2, spam=None, 3] # SyntaxError
obj[1, 2, spam=None, 3] # SyntaxError
This is like function calls, where intermixing positional and keyword This is like function calls, where intermixing positional and keyword
arguments give a SyntaxError. arguments give a SyntaxError.
5. Keyword subscripts, if any, will be handled like they are in 5. Keyword subscripts, if any, will be handled like they are in
function calls. Examples: function calls. Examples::
:: # Single index with keywords:
obj[index, spam=1, eggs=2]
# calls type(obj).__getitem__(obj, index, spam=1, eggs=2)
obj[index, spam=1, eggs=2] = value
# calls type(obj).__setitem__(obj, index, value, spam=1, eggs=2)
# Single index with keywords: del obj[index, spam=1, eggs=2]
# calls type(obj).__delitem__(obj, index, spam=1, eggs=2)
obj[index, spam=1, eggs=2] # Comma-separated indices with keywords:
# calls type(obj).__getitem__(obj, index, spam=1, eggs=2)
obj[index, spam=1, eggs=2] = value obj[foo, bar, spam=1, eggs=2]
# calls type(obj).__setitem__(obj, index, value, spam=1, eggs=2) # calls type(obj).__getitem__(obj, (foo, bar), spam=1, eggs=2)
del obj[index, spam=1, eggs=2] obj[foo, bar, spam=1, eggs=2] = value
# calls type(obj).__delitem__(obj, index, spam=1, eggs=2) # calls type(obj).__setitem__(obj, (foo, bar), value, spam=1, eggs=2)
# Comma-separated indices with keywords: del obj[foo, bar, spam=1, eggs=2]
# calls type(obj).__detitem__(obj, (foo, bar), spam=1, eggs=2)
obj[foo, bar, spam=1, eggs=2]
# calls type(obj).__getitem__(obj, (foo, bar), spam=1, eggs=2)
obj[foo, bar, spam=1, eggs=2] = value
# calls type(obj).__setitem__(obj, (foo, bar), value, spam=1, eggs=2)
del obj[foo, bar, spam=1, eggs=2]
# calls type(obj).__detitem__(obj, (foo, bar), spam=1, eggs=2)
Note that: Note that:
@ -431,11 +376,9 @@ The following old semantics are preserved:
- but if no ``**kwargs`` parameter is defined, it is an error. - but if no ``**kwargs`` parameter is defined, it is an error.
7. Sequence unpacking remains a syntax error inside subscripts: 7. Sequence unpacking remains a syntax error inside subscripts::
:: obj[*items]
obj[*items]
Reason: unpacking items would result it being immediately repacked into Reason: unpacking items would result it being immediately repacked into
a tuple. Anyone using sequence unpacking in the subscript is probably a tuple. Anyone using sequence unpacking in the subscript is probably
@ -445,56 +388,43 @@ The following old semantics are preserved:
This restriction has however been considered arbitrary by some, and it might This restriction has however been considered arbitrary by some, and it might
be lifted at a later stage for symmetry with kwargs unpacking, see next. be lifted at a later stage for symmetry with kwargs unpacking, see next.
8. Dict unpacking is permitted: 8. Dict unpacking is permitted::
:: items = {'spam': 1, 'eggs': 2}
obj[index, **items]
# equivalent to obj[index, spam=1, eggs=2]
items = {'spam': 1, 'eggs': 2} 9. Keyword-only subscripts are permitted. The positional index will be the empty tuple::
obj[index, **items]
# equivalent to obj[index, spam=1, eggs=2]
obj[spam=1, eggs=2]
# calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
obj[spam=1, eggs=2] = 5
# calls type(obj).__setitem__(obj, (), 5, spam=1, eggs=2)
del obj[spam=1, eggs=2]
# calls type(obj).__delitem__(obj, (), spam=1, eggs=2)
9. Keyword-only subscripts are permitted. The positional index will be the empty tuple: 10. Keyword arguments must allow slice syntax::
:: obj[3:4, spam=1:4, eggs=2]
# calls type(obj).__getitem__(obj, slice(3, 4, None), spam=slice(1, 4, None), eggs=2)
obj[spam=1, eggs=2]
# calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
obj[spam=1, eggs=2] = 5
# calls type(obj).__setitem__(obj, (), 5, spam=1, eggs=2)
del obj[spam=1, eggs=2]
# calls type(obj).__delitem__(obj, (), spam=1, eggs=2)
10. Keyword arguments must allow slice syntax.
::
obj[3:4, spam=1:4, eggs=2]
# calls type(obj).__getitem__(obj, slice(3, 4, None), spam=slice(1, 4, None), eggs=2)
This may open up the possibility to accept the same syntax for general function This may open up the possibility to accept the same syntax for general function
calls, but this is not part of this recommendation. calls, but this is not part of this recommendation.
11. Keyword arguments must allow Ellipsis 11. Keyword arguments must allow Ellipsis::
:: obj[..., spam=..., eggs=2]
# calls type(obj).__getitem__(obj, Ellipsis, spam=Ellipsis, eggs=2)
obj[..., spam=..., eggs=2] 12. Keyword arguments allow for default values::
# calls type(obj).__getitem__(obj, Ellipsis, spam=Ellipsis, eggs=2)
# Given type(obj).__getitem__(obj, index, spam=True, eggs=2)
12. Keyword arguments allow for default values obj[3] # Valid. index = 3, spam = True, eggs = 2
obj[3, spam=False] # Valid. index = 3, spam = False, eggs = 2
:: obj[spam=False] # Valid. index = (), spam = False, eggs = 2
obj[] # Invalid.
# Given type(obj).__getitem__(obj, index, spam=True, eggs=2)
obj[3] # Valid. index = 3, spam = True, eggs = 2
obj[3, spam=False] # Valid. index = 3, spam = False, eggs = 2
obj[spam=False] # Valid. index = (), spam = False, eggs = 2
obj[] # Invalid.
13. The same semantics given above must be extended to ``__class__getitem__``: 13. The same semantics given above must be extended to ``__class__getitem__``:
Since PEP 560, type hints are dispatched so that for ``x[y]``, if no Since PEP 560, type hints are dispatched so that for ``x[y]``, if no
@ -517,18 +447,14 @@ Corner case and Gotchas
With the introduction of the new notation, a few corner cases need to be analysed: With the introduction of the new notation, a few corner cases need to be analysed:
1. Technically, if a class defines their getter like this: 1. Technically, if a class defines their getter like this::
:: def __getitem__(self, index):
def __getitem__(self, index): then the caller could call that using keyword syntax, like these two cases::
then the caller could call that using keyword syntax, like these two cases: obj[3, index=4]
obj[index=1]
::
obj[3, index=4]
obj[index=1]
The resulting behavior would be an error automatically, since it would be like The resulting behavior would be an error automatically, since it would be like
attempting to call the method with two values for the ``index`` argument, and attempting to call the method with two values for the ``index`` argument, and
@ -540,19 +466,14 @@ With the introduction of the new notation, a few corner cases need to be analyse
backward compatibility issues on this respect. backward compatibility issues on this respect.
Classes that wish to stress this behavior explicitly can define their Classes that wish to stress this behavior explicitly can define their
parameters as positional-only: parameters as positional-only::
:: def __getitem__(self, index, /):
def __getitem__(self, index, /): 2. a similar case occurs with setter notation::
2. a similar case occurs with setter notation # Given type(obj).__getitem__(self, index, value):
obj[1, value=3] = 5
::
# Given type(obj).__getitem__(self, index, value):
obj[1, value=3] = 5
This poses no issue because the value is passed automatically, and the python interpreter will raise This poses no issue because the value is passed automatically, and the python interpreter will raise
``TypeError: got multiple values for keyword argument 'value'`` ``TypeError: got multiple values for keyword argument 'value'``
@ -560,48 +481,36 @@ With the introduction of the new notation, a few corner cases need to be analyse
3. If the subscript dunders are declared to use positional-or-keyword 3. If the subscript dunders are declared to use positional-or-keyword
parameters, there may be some surprising cases when arguments are passed parameters, there may be some surprising cases when arguments are passed
to the method. Given the signature: to the method. Given the signature::
:: def __getitem__(self, index, direction='north')
def __getitem__(self, index, direction='north') if the caller uses this::
if the caller uses this: obj[0, 'south']
:: they will probably be surprised by the method call::
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')
# 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 Solution: best practice suggests that keyword subscripts should be
flagged as keyword-only when possible: flagged as keyword-only when possible::
:: def __getitem__(self, index, *, direction='north')
def __getitem__(self, index, *, direction='north')
The interpreter need not enforce this rule, as there could be scenarios The interpreter need not enforce this rule, as there could be scenarios
where this is the desired behaviour. But linters may choose to warn where this is the desired behaviour. But linters may choose to warn
about subscript methods which don't use the keyword-only flag. about subscript methods which don't use the keyword-only flag.
4. As we saw, a single value followed by a keyword argument will not be changed into a tuple, i.e.: 4. As we saw, a single value followed by a keyword argument will not be changed into a tuple, i.e.:
``d[1, a=3]`` is treated as ``__getitem__(1, a=3)``, NOT ``__getitem__((1,), a=3)``. It would be ``d[1, a=3]`` is treated as ``__getitem__(1, a=3)``, NOT ``__getitem__((1,), a=3)``. It would be
extremely confusing if adding keyword arguments were to change the type of the passed index. extremely confusing if adding keyword arguments were to change the type of the passed index.
In other words, adding a keyword to a single-valued subscript will not change it into a tuple. In other words, adding a keyword to a single-valued subscript will not change it into a tuple.
For those cases where an actual tuple needs to be passed, a proper syntax will have to be used: For those cases where an actual tuple needs to be passed, a proper syntax will have to be used::
:: obj[(1,), a=3] # calls __getitem__((1,), a=3)
obj[(1,), a=3] # calls __getitem__((1,), a=3)
In this case, the call is passing a single element (which is passed as is, as from rule above), In this case, the call is passing a single element (which is passed as is, as from rule above),
only that the single element happens to be a tuple. only that the single element happens to be a tuple.
@ -609,31 +518,25 @@ With the introduction of the new notation, a few corner cases need to be analyse
Note that this behavior just reveals the truth that the ``obj[1,]`` notation is shorthand for Note that this behavior just reveals the truth that the ``obj[1,]`` notation is shorthand for
``obj[(1,)]`` (and also ``obj[1]`` is shorthand for ``obj[(1)]``, with the expected behavior). ``obj[(1,)]`` (and also ``obj[1]`` is shorthand for ``obj[(1)]``, with the expected behavior).
When keywords are present, the rule that you can omit this outermost pair of parentheses is no When keywords are present, the rule that you can omit this outermost pair of parentheses is no
longer true. longer true::
:: obj[1] # calls __getitem__(1)
obj[1, a=3] # calls __getitem__(1, a=3)
obj[1,] # calls __getitem__((1,))
obj[(1,), a=3] # calls __getitem__((1,), a=3)
obj[1] # calls __getitem__(1) This is particularly relevant in the case where two entries are passed::
obj[1, a=3] # calls __getitem__(1, a=3)
obj[1,] # calls __getitem__((1,))
obj[(1,), a=3] # calls __getitem__((1,), a=3)
This is particularly relevant in the case where two entries are passed: obj[1, 2] # calls __getitem__((1, 2))
obj[(1, 2)] # same as above
obj[1, 2, a=3] # calls __getitem__((1, 2), a=3)
obj[(1, 2), a=3] # calls __getitem__((1, 2), a=3)
:: And particularly when the tuple is extracted as a variable::
obj[1, 2] # calls __getitem__((1, 2)) t = (1, 2)
obj[(1, 2)] # same as above obj[t] # calls __getitem__((1, 2))
obj[1, 2, a=3] # calls __getitem__((1, 2), a=3) obj[t, a=3] # calls __getitem__((1, 2), a=3)
obj[(1, 2), a=3] # calls __getitem__((1, 2), a=3)
And particularly when the tuple is extracted as a variable:
::
t = (1, 2)
obj[t] # calls __getitem__((1, 2))
obj[t, a=3] # calls __getitem__((1, 2), a=3)
Why? because in the case ``obj[1, 2, a=3]`` we are passing two elements (which Why? because in the case ``obj[1, 2, a=3]`` we are passing two elements (which
are then packed as a tuple and passed as the index). In the case ``obj[(1, 2), a=3]`` are then packed as a tuple and passed as the index). In the case ``obj[(1, 2), a=3]``
@ -663,7 +566,6 @@ to invoke the old functions. We propose ``BINARY_SUBSCR_EX``,
have to generate these new opcodes. The ``PyObject_(Get|Set|Del)Item`` implementations have to generate these new opcodes. The ``PyObject_(Get|Set|Del)Item`` implementations
will call the extended methods passing ``NULL`` as kwargs. will call the extended methods passing ``NULL`` as kwargs.
Workarounds Workarounds
=========== ===========
@ -680,68 +582,52 @@ be universal. For example, a module or package might require the use of its own
helpers. helpers.
1. User defined classes can be given ``getitem`` and ``delitem`` methods, 1. User defined classes can be given ``getitem`` and ``delitem`` methods,
that respectively get and delete values stored in a container. that respectively get and delete values stored in a container::
:: >>> val = x.getitem(1, 2, a=3, b=4)
>>> x.delitem(1, 2, a=3, b=4)
>>> val = x.getitem(1, 2, a=3, b=4) The same can't be done for ``setitem``. It's not valid syntax::
>>> x.delitem(1, 2, a=3, b=4)
The same can't be done for ``setitem``. It's not valid syntax. >>> x.setitem(1, 2, a=3, b=4) = val
SyntaxError: can't assign to function call
::
>>> x.setitem(1, 2, a=3, b=4) = val
SyntaxError: can't assign to function call
2. A helper class, here called ``H``, can be used to swap the container 2. A helper class, here called ``H``, can be used to swap the container
and parameter roles. In other words, we use and parameter roles. In other words, we use::
:: H(1, 2, a=3, b=4)[x]
H(1, 2, a=3, b=4)[x] as a substitute for::
as a substitute for x[1, 2, a=3, b=4]
::
x[1, 2, a=3, b=4]
This method will work for ``getitem``, ``delitem`` and also for This method will work for ``getitem``, ``delitem`` and also for
``setitem``. This is because ``setitem``. This is because::
:: >>> H(1, 2, a=3, b=4)[x] = val
>>> H(1, 2, a=3, b=4)[x] = val
is valid syntax, which can be given the appropriate semantics. is valid syntax, which can be given the appropriate semantics.
3. A helper function, here called ``P``, can be used to store the 3. A helper function, here called ``P``, can be used to store the
arguments in a single object. For example arguments in a single object. For example::
:: >>> x[P(1, 2, a=3, b=4)] = val
>>> x[P(1, 2, a=3, b=4)] = val
is valid syntax, and can be given the appropriate semantics. is valid syntax, and can be given the appropriate semantics.
4. The ``lo:hi:step`` syntax for slices is sometimes very useful. This 4. The ``lo:hi:step`` syntax for slices is sometimes very useful. This
syntax is not directly available in the work-arounds. However syntax is not directly available in the work-arounds. However::
::
s[lo:hi:step] s[lo:hi:step]
provides a work-around that is available everything, where provides a work-around that is available everything, where::
:: class S:
def __getitem__(self, key): return key
s = S()
class S: defines the helper object ``s``.
def __getitem__(self, key): return key
s = S()
defines the helper object `s`.
Rejected Ideas Rejected Ideas
============== ==============
@ -771,19 +657,15 @@ that are invoked over the ``__(get|set|del)item__`` triad, if they are present.
The rationale around this choice is to make the intuition around how to add kwd The rationale around this choice is to make the intuition around how to add kwd
arg support to square brackets more obvious and in line with the function arg support to square brackets more obvious and in line with the function
behavior. Given: behavior. Given::
:: def __getitem_ex__(self, x, y): ...
def __getitem_ex__(self, x, y): ... These all just work and produce the same result effortlessly::
These all just work and produce the same result effortlessly: obj[1, 2]
obj[1, y=2]
:: obj[y=2, x=1]
obj[1, 2]
obj[1, y=2]
obj[y=2, x=1]
In other words, this solution would unify the behavior of ``__getitem__`` to the traditional In other words, this solution would unify the behavior of ``__getitem__`` to the traditional
function signature, but since we can't change ``__getitem__`` and break backward compatibility, function signature, but since we can't change ``__getitem__`` and break backward compatibility,
@ -813,14 +695,12 @@ The problems with this approach were found to be:
- it would potentially lead to mixed situations where the extended version is - it would potentially lead to mixed situations where the extended version is
defined for the getter, but not for the setter. defined for the getter, but not for the setter.
- In the __setitem_ex__ signature, value would have to be made the first - In the ``__setitem_ex__`` signature, value would have to be made the first
element, because the index is of arbitrary length depending on the specified element, because the index is of arbitrary length depending on the specified
indexes. This would look awkward because the visual notation does not match indexes. This would look awkward because the visual notation does not match
the signature: the signature::
:: obj[1, 2] = 3 # calls obj.__setitem_ex__(3, 1, 2)
obj[1, 2] = 3 # calls obj.__setitem_ex__(3, 1, 2)
- the solution relies on the assumption that all keyword indices necessarily map - the solution relies on the assumption that all keyword indices necessarily map
into positional indices, or that they must have a name. This assumption may be into positional indices, or that they must have a name. This assumption may be
@ -842,11 +722,9 @@ Has problems similar to the above.
create a new "kwslice" object create a new "kwslice" object
----------------------------- -----------------------------
This proposal has already been explored in "New arguments contents" P4 in PEP 472. This proposal has already been explored in "New arguments contents" P4 in PEP 472::
:: obj[a, b:c, x=1] # calls __getitem__(a, slice(b, c), key(x=1))
obj[a, b:c, x=1] # calls __getitem__(a, slice(b, c), key(x=1))
This solution requires everyone who needs keyword arguments to parse the tuple This solution requires everyone who needs keyword arguments to parse the tuple
and/or key object by hand to extract them. This is painful and opens up to the and/or key object by hand to extract them. This is painful and opens up to the
@ -858,24 +736,18 @@ make sense and which ones do not.
Using a single bit to change the behavior Using a single bit to change the behavior
----------------------------------------- -----------------------------------------
A special class dunder flag A special class dunder flag::
::
__keyfn__ = True __keyfn__ = True
would change the signature of the ``__get|set|delitem__`` to a "function like" dispatch, would change the signature of the ``__get|set|delitem__`` to a "function like" dispatch,
meaning that this meaning that this::
:: >>> d[1, 2, z=3]
>>> d[1, 2, z=3] would result in a call to::
would result in a call to >>> d.__getitem__(1, 2, z=3) # instead of d.__getitem__((1, 2), z=3)
::
>>> d.__getitem__(1, 2, z=3) # instead of d.__getitem__((1, 2), z=3)
This option has been rejected because it feels odd that a signature of a method This option has been rejected because it feels odd that a signature of a method
depends on a specific value of another dunder. It would be confusing for both depends on a specific value of another dunder. It would be confusing for both
@ -916,47 +788,37 @@ Use None instead of the empty tuple when no positional index is given
The case ``obj[k=3]`` will lead to a call ``__getitem__((), k=3)``. The case ``obj[k=3]`` will lead to a call ``__getitem__((), k=3)``.
The alternative ``__getitem__(None, k=3)`` was considered but rejected: The alternative ``__getitem__(None, k=3)`` was considered but rejected:
NumPy uses `None` to indicate inserting a new axis/dimensions (there's NumPy uses `None` to indicate inserting a new axis/dimensions (there's
a ``np.newaxis`` alias as well): a ``np.newaxis`` alias as well)::
:: arr = np.array(5)
arr.ndim == 0
arr[None].ndim == arr[None,].ndim == 1
arr = np.array(5) So the final conclusion is that we favor the following series::
arr.ndim == 0
arr[None].ndim == arr[None,].ndim == 1
So the final conclusion is that we favor the following series: obj[k=3] # __getitem__((), k=3). Empty tuple
obj[1, k=3] # __getitem__(1, k=3). Integer
obj[1, 2, k=3] # __getitem__((1, 2), k=3). Tuple
:: more than this::
obj[k=3] # __getitem__((), k=3). Empty tuple obj[k=3] # __getitem__(None, k=3). None
obj[1, k=3] # __getitem__(1, k=3). Integer obj[1, k=3] # __getitem__(1, k=3). Integer
obj[1, 2, k=3] # __getitem__((1, 2), k=3). Tuple obj[1, 2, k=3] # __getitem__((1, 2), k=3). Tuple
more than this:
::
obj[k=3] # __getitem__(None, k=3). None
obj[1, k=3] # __getitem__(1, k=3). Integer
obj[1, 2, k=3] # __getitem__((1, 2), k=3). Tuple
With the first more in line with a \*args semantics for calling a routine with With the first more in line with a \*args semantics for calling a routine with
no positional arguments no positional arguments::
:: >>> def foo(*args, **kwargs):
... print(args, kwargs)
...
>>> foo(k=3)
() {'k': 3}
>>> def foo(*args, **kwargs): Although we accept the following asymmetry::
... print(args, kwargs)
...
>>> foo(k=3)
() {'k': 3}
Although we accept the following asymmetry: >>> foo(1, k=3)
(1,) {'k': 3}
::
>>> foo(1, k=3)
(1,) {'k': 3}
Common objections Common objections
@ -971,18 +833,14 @@ Common objections
One problem is type hint creation has been extended to built-ins in python 3.9, One problem is type hint creation has been extended to built-ins in python 3.9,
so that you do not have to import Dict, List, et al anymore. so that you do not have to import Dict, List, et al anymore.
Without kwdargs inside ``[]``, you would not be able to do this: Without kwdargs inside ``[]``, you would not be able to do this::
:: Vector = dict[i=float, j=float]
Vector = dict[i=float, j=float]
but for obvious reasons, call syntax using builtins to create custom type hints but for obvious reasons, call syntax using builtins to create custom type hints
isn't an option: isn't an option::
:: dict(i=float, j=float) # would create a dictionary, not a type
dict(i=float, j=float) # would create a dictionary, not a type
References References
========== ==========