It's really a strength, not a quirk. Negative indexing and array slicing in general are great in Python. Really easy to pick up and way more convenient than any other language that I've come across.
Negative indexing in Python is a dangerous design that hides bugs and causes incorrect programs to return garbage instead of erroring. If you have an incorrect index computation, instead of getting a bounds error, you get different indexing behavior. This makes it dangerously easy to write code that appears to work but computes nonsense.
Without checking, I'm not sure whether a[-1:0:-1] reverses a list. I'm not sure if it includes the first element of the list or not [edit: It doesn't]. I'm not sure why in contrast a[::-1] does reverse a list. I found array slicing (and ranges) to be a source of confusion when programming the aforementioned linear algebra algorithms.
IMO, the Julia approach is better: 3:-1:1 produces [3,2,1]. Both the starting and ending points of the range are included.
The example a[-1:0:-1] should be pretty understandable once you've spent enough time in the language -- you're inclusive at -1 and exclusive at 0, so the list is reversed and is missing its formerly first element (should one exist).
That logic is pretty consistent. The start is always inclusive, so it needs to be `len(a)-`, `-`, or `None`. The end is always exclusive, so you need to choose `None`. As a result, counting all the syntactic sugar available to you, you have 8 slices that can reverse a list. To name a few you have a[::-1], a[None:None:-1], and a[-1::-1].
IMO a much more interesting example for the strengths of inclusive indexes is a[len(a)-1:-1:-1]. The result is always empty, but it wouldn't be too much of a stretch to think you included len(a)-1, you decremented to -1 exclusively (thus including 0), and hence reversed the entire list. The problem is that -1 is a valid index, and unlike the a[0:len(a)] case you don't have any values "before" the beginning of the list to include 0 in an _exclusive_ expression.
It's all a bit of a moot point though. I know Python especially chose its [inclusive,exclusive) convention largely because it wanted expressions like range(len(a)) to not require additional arithmetic for common use cases given that it had zero-based indexing. Julia has one-based indexing, so for common use cases an [inclusive,inclusive] pattern falls out as the most natural choice. I have no idea if Julia actually cared about that sort of thing or if such a convention came about by accident, but it seems like a clean choice for a one-based language.
Ohm, a lot of thought has gone into indexing in Julia. Julia allows indexing by position `A[1,1]`, slicing `A[1:5, :]`, linear indexing `A[1]`, and logical indexing `A[a.==1]`, relative indexing `A[end-1]` and cartesian indexing `A[CartesianIndex(1,1)]`.
The way Julia combines these in a mostly non-conflicting and non-confusing manner is a major engineering feat.
For example the following rule was found to be the just the right balance between permissive and strict behaviour:
* You are permitted to index into arrays with more indices than dimensions, but all trailing indices must be 1.
* You are permitted to index into arrays with fewer indices than dimensions, but the length of all the omitted dimensions must be 1.
I don't know; the grammar is already pretty complicated without introducing additional indexing schemes. Now that you mention it though, I don't know that I've ever written an algorithm mixing and matching negative/positive indices in a way that couldn't be trivially re-written with that kind of syntax. I'm sure such cases exist for somebody...
Looking for solutions, if you're stuck with Python and hate that behavior:
- If you don't need errors raised then a[~i] is already equal to a[-i-1].
- In your own code (or at its boundaries when wrapping external lists) it's nearly free and not much code to subclass list, override __getitem__, and raise errors for negative indices while responding to some syntax like a[0,] or a.R[0].