Type Confusion¶
When using the ndindex API, it is important to avoid type confusion. Many types that are used as indices for arrays also have semantic meaning outside of indexing. For example, tuples and arrays mean one thing when they are indices, but they are also used in contexts that have nothing to do with indexing.
ndindex classes have names that are based on the native class names for the index type they represent. One must be careful, however, to not confuse these classes with the classes they represent. Most methods that work on the native classes are not available on the ndindex classes.
Some general types to help avoid type confusion:
Always use the
ndindex()function to create ndindex types. When calling ndindex methods or creatingTupleobjects, it is not necessary to convert arguments to ndindex types first. Slice literals (using:) are not valid syntax outside of a getitem (square brackets), but you can use theslicebuilt-in object to create slices.slice(a, b, c)is the same asa:b:c.Wrong:
idx.as_subindex(Tuple(Integer(1))) # More verbose than necessary
Right:
idx.as_subindex((1,))
Keep all index objects as ndindex types until performing actual indexing. If all you are doing with an index is indexing it, and not manipulating it or storing it somewhere else, there is no need to use ndindex. But if you are storing ndindex types separately before indexing, or plan to manipulate them in any way, it is best to convert them to ndindex types with
ndindex()as early as possible, and store them in that form. Only convert back to a raw index (with.raw, see the next bullet point) once doing an actual index operation. Avoid mixing ndindex and “raw” or non-ndindex types. There are many reasons to avoid this:Raw types (such as
int,slice,tuple,array, and so on), do not have any of the same methods as ndindex, so your code may fail.Some raw types, such as slices, arrays, and tuples containing slices or arrays, are not hashable, so if you try to use them as a dictionary key, they will fail. ndindex types are always hashable.
Wrong
# Fails with a TypeError: unhashable type: 'slice' indices = { slice(0, 10): 0, slice(10, 20): 1 }
Right
indices = { ndindex[0:10]: 0, ndindex[10:20]: 1 }
ndindex does basic type checking on indices that would otherwise not happen until they are actually used as an index. For example,
slice(1.0, 2.0)does not fail until you try to index an array with it, butSlice(1.0, 2.0)fails immediately.Wrong
# Typo would not be caught until idx is actually used to index an array idx = slice(1, 2.)
Right
# Typo would be caught right away idx = ndindex[1:2.] # OR idx = Slice(1, 2.)
NumPy arrays and tuples containing NumPy arrays are not easy to compare, since using
==on anarraydoes not produce a boolean.==on an ndindex type will always produce a boolean, which compares if the two indices are exactly equal.Wrong
# Fails with ValueError: The truth value of an array with more than one element is ambiguous. if idx == (slice(0, 2), np.array([0, 0]): ...
Right
# Note only one side of the == needs to be an ndindex type if ndindex(idx) == (slice(0, 2), np.array([0, 0]): ...
Additionally, all ndindex types are immutable, including types representing NumPy arrays, so it is impossible to accidentally mutate an ndindex array index object.
Use
.rawto convert an ndindex object to an indexable type. With the exception ofInteger, it is impossible for custom types to define themselves as indices to NumPy arrays, so it is necessary to usea[idx.raw]rather thana[idx]whenidxis an ndindex type. Since native index types do not have a.rawmethod, it is recommended to always keep any index object that you are using as an ndindex type, and use.rawonly when you need to use it as an index. If you get the error “IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices”, it indicates you forgot to use.raw.Right:
a[idx.raw]
Wrong:
a[idx] # Gives an error
Use ndindex classes only for objects that represent indices. Do not use classes like
Integer,Tuple,IntegerArray, orBooleanArrayunless the object in question is going to be an index to an array. For example, array shapes are always tuples of integers, but they are not indices, soTupleshould not be used to represent an array shape, but rather just a normaltuple. If an object will be used both as an index and an integer/tuple/array in its own right, either make use of.rawwhen using it in non-index contexts, or store the objects separately (note thatIntegerArrayandBooleanArrayalways make a copy of the input argument, so there is no issue with managing it separately).Right:
np.empty((1, 2, 3)) # idx is an ndindex object idx.newshape((1, 2, 3))
Wrong:
np.empty(Tuple(1, 2, 3)) # gives an error # idx is an ndindex object idx.newshape(Tuple(1, 2, 3)) # gives an error
Try to use ndindex methods to manipulate indices. The whole reason ndindex exists is that writing formulas for manipulating indices is hard, and it’s easy to get the corner cases wrong. ndindex is rigorously tested so you can be highly confident of its correctness. If you find yourself manipulating index args directly in complex ways, it’s a sign you should probably be using a higher level abstraction. If what you are trying to do doesn’t exist yet, open an issue so we can implement it.
Additionally, some advice for specific types:
Integer¶
Integershould not be thought of as an int type. It represents integers as indices. It is not usable in other contexts where ints are usable. For example, arithmetic will not work on it. If you need to manipulate theIntegerindex as anint, useidx.raw.Right:
idx = ndindex(0) idx.raw + 1
Wrong:
idx = ndindex(0) idx + 1 # Produces an error
Integeris the only index type that can be used directly as an array index. This is because the__index__API allows custom integer objects to define themselves as indices. However, this API does not extend to other index types like slices or tuples. It is recommended to always useidx.raweven ifidxis anInteger, so that it will also work even if it is another index type. You should not rely on any ndindex function returning a specific index type (unless it states that it does so in its docstring).
Tuple¶
Tupleshould not be thought of as a tuple. In particular, things likeidx[0]andlen(idx)will not work ifidxis aTuple. If you need to access the specific term in aTuple, useTuple.argsif you want the ndindex type, orTuple.rawif you want the raw type.Right:
idx = ndindex[0, 0:1] idx.raw[0] # Gives int(0) idx.args[0] # Gives Integer(0)
Wrong:
idx = ndindex[0, 0:1] idx[0] # Produces an error
Tupleis defined asTuple(*args).Tuple(args)gives an error.Right:
Tuple(0, slice(0, 1))
Better:
ndindex[0, 0:1]
Wrong:
Tuple((0, slice(0, 1))) # Gives an error
ellipsis¶
You should almost never use the ndindex
ellipsisclass directly. Instead, use...orndindex[...]. As noted above, all ndindex methods andTuplewill automatically convert...into the ndindex type.Right:
idx = ndindex(...) idx.reduce()
Wrong:
idx = ... idx.reduce() # Gives an error
We recommend preferring
...over the built-in nameEllipsis, as it is more readable. The...syntax is allowed everywhere thatEllipsiswould work.Right:
idx = ndindex[0, ..., 1]
Wrong:
idx = ndindex[0, Ellipsis, 1] # Less readable
If you do use
ellipsisbeware that it is the class, not the instance, unlike the built-inEllipsisobject. This is done for consistency in the internal ndindex class hierarchy.Right:
idx = ndindex[0, ..., 1]
Wrong:
idx = ndindex[0, ellipsis, 1] # Gives an error
The below do not give errors, but it is easy to confuse them with the above. It is best to just use
..., which is more concise and easier to read.idx = ndindex[0, ellipsis(), 1] # Easy to confuse, less readable idx.reduce()
idx = ndindex[0, Ellipsis, 1] # Easy to confuse, less readable idx.reduce()
ellipsisis not singletonized, unlike the built-in.... Aside from singletonization not being necessary for ndindex types, it would be impossible to makeellipsis() is ...return True. If you are using ndindex, you should use==to compare against..., and avoid usingis. Note that as long as you knowidxis an ndindex type, this is safe to do, since even the array index typesIntegerArrayandBooleanArrayallow==comparison (unlike NumPy arrays).Right:
if idx == ...:
Wrong:
if idx is Ellipsis: # Will be False if idx is the ndindex ellipsis type
if idx is ellipsis(): # Will always be False (ellipsis() creates a new instance)
Newaxis¶
The advice for Newaxis is almost identical to the advice for
ellipsis. Note that np.newaxis is just an alias
for None.
You should almost never use the ndindex
Newaxisclass directly. Instead, usenp.newaxis,None,ndindex(np.newaxis), orndindex(None). As noted above, all ndindex methods andTuplewill automatically convertNoneinto the ndindex type.Right:
idx = ndindex[np.newaxis] idx.reduce()
Wrong:
idx = np.newaxis idx.reduce() # Gives an error
If you do use
Newaxisbeware that it is the class, not the instance, unlike the NumPynp.newaxisobject (i.e.,None). This is done for consistency in the internal ndindex class hierarchy.Right:
idx = ndindex[0, np.newaxis, 1]
idx = ndindex[0, None, 1]
Wrong:
idx = ndindex[0, Newaxis, 1] # Gives an error
The below does not give an error, but it is easy to confuse it with the above. It is best to just use
np.newaxisorNone, which is more concise and easier to read.idx = ndindex[0, Newaxis(), 1] # Easy to confuse idx.reduce()
Newaxisis not singletonized, unlike the built-inNone. It would also be impossible to makeNewaxis() is np.newaxisorNewaxis() is Nonereturn True. If you are using ndindex, you should use==to compare againstnp.newaxisorNone, and avoid usingis. Note that as long as you knowidxis an ndindex type, this is safe to do, since even the array index typesIntegerArrayandBooleanArrayallow==comparison (unlike NumPy arrays).Right:
if idx == np.newaxis:
if idx == None:
Wrong:
if idx is np.newaxis: # Will be False if idx is the ndindex Newaxis type
if idx is None: # Will be False if idx is the ndindex Newaxis type
if idx is Newaxis(): # Will be False (Newaxis() creates a new instance)
IntegerArray and BooleanArray¶
IntegerArrayandBooleanArrayshould not be thought of as arrays. They do not have the methods thatnumpy.ndarraywould have. They also have fixed dtypes (intpandbool_) and are restricted by what is allowed as indices by NumPy. To access the arrays they represent, useidx.arrayoridx.raw.Right:
idx = IntegerArray(np.array([0, 1])) idx.array[0]
Wrong:
idx = IntegerArray(np.array([0, 1])) idx[0] # Gives an error
Like all other ndindex types,
IntegerArrayandBooleanArrayare immutable.. The.arrayobject on them is set as read-only to enforce this. To modify an array index, create a new object. All ndindex methods that manipulate indices, likereduce(), return new objects. If you create anIntegerArrayorBooleanArrayobject out of an existing array, the array is copied so that modifications to the original array do not affect the ndindex objects.Right:
idx = IntegerArray(np.array([0, 1])) arr = idx.array.copy() arr[0] = 1 idx2 = IntegerArray(arr)
Wrong:
idx = IntegerArray(np.array([0, 1])) idx.array[0] = 1 # Gives an error