PEP 637: Renamed file to rst and changed formatting of block code (#1619)
This commit is contained in:
parent
ef4663634e
commit
8e16d32598
|
@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
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]
|
||||
>>> 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]):
|
||||
2. To enrich the typing notation with keywords, especially during the use of generics::
|
||||
|
||||
def function(value: MyType[T=int]):
|
||||
|
||||
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
|
||||
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
|
||||
>>> # new syntax
|
||||
>>> da[space=0, time=:2] = spam
|
||||
|
||||
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
|
||||
>>> # 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
|
||||
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
|
||||
``obj(x)``. The current python syntax focuses exclusively on position to express
|
||||
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, 2] # multiple indexes (for multidimensional arrays)
|
||||
>>> a[3] # returns the fourth element of 'a'
|
||||
>>> 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
|
||||
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
|
||||
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
|
||||
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]
|
||||
>>> a[1, 2] = 5
|
||||
but only the first one of these is valid::
|
||||
|
||||
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
|
||||
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
|
||||
have anonymous argument semantic, exactly like the indexing operation. However,
|
||||
__(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"
|
||||
in the ``index`` argument:
|
||||
in the ``index`` argument::
|
||||
|
||||
::
|
||||
|
||||
>>> x=X()
|
||||
>>> x[0]
|
||||
0
|
||||
>>> x[0, 1]
|
||||
(0, 1)
|
||||
>>> x[(0, 1)]
|
||||
(0, 1)
|
||||
>>>
|
||||
>>> x[()]
|
||||
()
|
||||
>>> x[{1, 2, 3}]
|
||||
{1, 2, 3}
|
||||
>>> x["hello"]
|
||||
hello
|
||||
>>> x["hello", "hi"]
|
||||
('hello', 'hi')
|
||||
>>> x=X()
|
||||
>>> x[0]
|
||||
0
|
||||
>>> x[0, 1]
|
||||
(0, 1)
|
||||
>>> x[(0, 1)]
|
||||
(0, 1)
|
||||
>>>
|
||||
>>> x[()]
|
||||
()
|
||||
>>> x[{1, 2, 3}]
|
||||
{1, 2, 3}
|
||||
>>> x["hello"]
|
||||
hello
|
||||
>>> x["hello", "hi"]
|
||||
('hello', 'hi')
|
||||
|
||||
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::
|
||||
|
||||
::
|
||||
|
||||
f()
|
||||
|
||||
this one isn't
|
||||
|
||||
::
|
||||
|
||||
a[]
|
||||
a[]
|
||||
|
||||
|
||||
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
|
||||
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, 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=..., 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.
|
||||
|
||||
|
@ -326,81 +282,70 @@ Syntax and Semantics
|
|||
|
||||
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)
|
||||
|
||||
obj[index]
|
||||
# calls type(obj).__getitem__(obj, index)
|
||||
del obj[index]
|
||||
# calls type(obj).__delitem__(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.
|
||||
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:
|
||||
a single positional argument::
|
||||
|
||||
::
|
||||
obj[spam, eggs]
|
||||
# calls type(obj).__getitem__(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))
|
||||
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
|
||||
arguments in subscripts need do nothing at all, and the feature is therefore
|
||||
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
|
||||
arguments give a SyntaxError.
|
||||
|
||||
5. Keyword subscripts, if any, will be handled like they are in
|
||||
function calls. Examples:
|
||||
function calls. Examples::
|
||||
|
||||
::
|
||||
# Single index with keywords:
|
||||
|
||||
# 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]
|
||||
# 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)
|
||||
|
||||
obj[index, spam=1, eggs=2] = value
|
||||
# calls type(obj).__setitem__(obj, index, value, spam=1, eggs=2)
|
||||
del obj[index, spam=1, eggs=2]
|
||||
# calls type(obj).__delitem__(obj, index, spam=1, eggs=2)
|
||||
|
||||
del obj[index, spam=1, eggs=2]
|
||||
# calls type(obj).__delitem__(obj, index, spam=1, eggs=2)
|
||||
# Comma-separated indices with keywords:
|
||||
|
||||
# Comma-separated indices with keywords:
|
||||
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]
|
||||
# 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)
|
||||
|
||||
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)
|
||||
del obj[foo, bar, spam=1, eggs=2]
|
||||
# calls type(obj).__detitem__(obj, (foo, bar), spam=1, eggs=2)
|
||||
|
||||
Note that:
|
||||
|
||||
|
@ -431,11 +376,9 @@ The following old semantics are preserved:
|
|||
- 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
|
||||
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
|
||||
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}
|
||||
obj[index, **items]
|
||||
# equivalent to obj[index, spam=1, eggs=2]
|
||||
9. Keyword-only subscripts are permitted. The positional index will be the empty tuple::
|
||||
|
||||
obj[spam=1, eggs=2]
|
||||
# calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
|
||||
|
||||
9. Keyword-only subscripts are permitted. The positional index will be the empty tuple:
|
||||
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)
|
||||
|
||||
obj[spam=1, eggs=2]
|
||||
# calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
|
||||
10. Keyword arguments must allow slice syntax::
|
||||
|
||||
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)
|
||||
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
|
||||
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]
|
||||
# calls type(obj).__getitem__(obj, Ellipsis, spam=Ellipsis, eggs=2)
|
||||
12. Keyword arguments allow for default values::
|
||||
|
||||
|
||||
12. Keyword arguments allow for default values
|
||||
|
||||
::
|
||||
|
||||
# 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.
|
||||
# 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__``:
|
||||
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:
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
``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
|
||||
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']
|
||||
|
||||
::
|
||||
|
||||
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')
|
||||
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:
|
||||
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
|
||||
where this is the desired behaviour. But linters may choose to warn
|
||||
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.:
|
||||
``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.
|
||||
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),
|
||||
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
|
||||
``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
|
||||
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)
|
||||
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::
|
||||
|
||||
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))
|
||||
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:
|
||||
|
||||
::
|
||||
|
||||
t = (1, 2)
|
||||
obj[t] # calls __getitem__((1, 2))
|
||||
obj[t, a=3] # calls __getitem__((1, 2), a=3)
|
||||
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
|
||||
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
|
||||
will call the extended methods passing ``NULL`` as kwargs.
|
||||
|
||||
|
||||
Workarounds
|
||||
===========
|
||||
|
||||
|
@ -680,68 +582,52 @@ be universal. For example, a module or package might require the use of its own
|
|||
helpers.
|
||||
|
||||
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)
|
||||
>>> x.delitem(1, 2, a=3, b=4)
|
||||
The same can't be done for ``setitem``. It's not valid syntax::
|
||||
|
||||
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
|
||||
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
|
||||
``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.
|
||||
|
||||
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.
|
||||
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
class S:
|
||||
def __getitem__(self, key): return key
|
||||
s = S()
|
||||
|
||||
s = S()
|
||||
|
||||
defines the helper object `s`.
|
||||
defines the helper object ``s``.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
-----------------------------
|
||||
|
||||
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
|
||||
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
|
||||
-----------------------------------------
|
||||
|
||||
A special class dunder flag
|
||||
|
||||
::
|
||||
A special class dunder flag::
|
||||
|
||||
__keyfn__ = True
|
||||
|
||||
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
|
||||
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 alternative ``__getitem__(None, k=3)`` was considered but rejected:
|
||||
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)
|
||||
arr.ndim == 0
|
||||
arr[None].ndim == arr[None,].ndim == 1
|
||||
So the final conclusion is that we favor the following series::
|
||||
|
||||
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[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__(None, k=3). None
|
||||
obj[1, k=3] # __getitem__(1, k=3). Integer
|
||||
obj[1, 2, k=3] # __getitem__((1, 2), k=3). Tuple
|
||||
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
|
||||
no positional arguments
|
||||
no positional arguments::
|
||||
|
||||
::
|
||||
>>> def foo(*args, **kwargs):
|
||||
... print(args, kwargs)
|
||||
...
|
||||
>>> foo(k=3)
|
||||
() {'k': 3}
|
||||
|
||||
>>> def foo(*args, **kwargs):
|
||||
... print(args, kwargs)
|
||||
...
|
||||
>>> foo(k=3)
|
||||
() {'k': 3}
|
||||
Although we accept the following asymmetry::
|
||||
|
||||
Although we accept the following asymmetry:
|
||||
|
||||
::
|
||||
|
||||
>>> foo(1, k=3)
|
||||
(1,) {'k': 3}
|
||||
>>> foo(1, k=3)
|
||||
(1,) {'k': 3}
|
||||
|
||||
|
||||
Common objections
|
||||
|
@ -971,18 +833,14 @@ Common objections
|
|||
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.
|
||||
|
||||
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
|
||||
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
|
||||
==========
|
Loading…
Reference in New Issue