Numpy has adopted this behavior from Python's sequence indexing for which the rules are explained here (for some history see below). Specifically footnote (5) reads:
The slice of s
from i
to j
with step k
is defined as the sequence of items with index x = i + n*k
such that 0 <= n < (j-i)/k
. In other words, the indices are i
, i+k
, i+2*k
, i+3*k
and so on, stopping when j
is reached (but never including j
). When k
is positive, i
and j
are reduced to len(s)
if they are greater. When k
is negative, i
and j
are reduced to len(s) - 1
if they are greater. If i
or j
are omitted or None
, they become “end” values (which end depends on the sign of k
). Note, k
cannot be zero. If k
is None, it is treated like 1
.
So the indices are generated from multipliers n
subject to 0 <= n < (j-i)/k
. For your specific example (j-i)/k < 0
and hence no indices are computed.
For Numpy arrays a[i:i+m][::-1]
generates a view of the underlying array, i.e. it has negligible overhead and thus appears to be a valid solution. It clearly conveys the intent, namely "take a subarray of an array a
starting from some generic index i
to index i+m
(excluded) in reverse order".
Alternatively, you can use None
as the stop argument if i
is zero:
a[i+m-1:(None if i==0 else i-1):-1]
History
Originally, Python implemented slicing syntax via __getslice__
(see also here) which didn't allow a step argument, i.e. it only used the 2-argument form: a[i:j]
. This was implemented by built-in sequences such as list
. Back then, around 1995, the predecessor of Numpy, Numerical Python, was developed and discussed within the MATRIX-SIG (special interest group). This predecessor implemented a specific Slice
type which could be used to also specify a so called stride
(now step
) in a form very similar to today's slice
: e.g. a[Slice(None, None, 2)]
. It was asked to extend Python's syntax to allow for the 3-form slicing known today: a[::2]
(see e.g. this thread). This got implemented in form of the slice
type and would be passed to __getitem__
instead of __getslice__
. So back then, a[i:j]
was resolved as a.__getslice__(i, j)
while a[i:j:k]
was resolved as a.__getitem__(slice(i, j, k))
. Back then, Numerical Python even allowed "reverse" slicing with the 2-form, interpreting the second argument as the stride (see the docs; e.g. a[i:-1]
was equivalent to a[i::-1]
for an array object a
). Indexing of arrays was oriented at how indexing for Python sequences worked: including the start index, excluding the stop index (see here). This applied to negative stride (step) as well, hence providing the behavior that can be observed today. The decision was probably based on the principle of least surprise (for "standard" Python users).
It took a long time until Python 2.3 where the extended slicing feature including a step was implemented for the built-in types (see what's new and the docs; note that the 2.3 version of the docs contained a wrong description of slicing with step which was fixed for the 2.4 release).