So I was playing around with the numpy array data structure and it struck me that a summarizing piece of code, somewhere at the start of “Python Basics with Numpy”, might be of interest to the student who wants to be shown some multidimensional (really: multiaxial) array handling code. Maybe one could consider extending the exercise accordingly:
import numpy as np
def examine(m):
if isinstance(m, (int, float, complex)):
# NUMERIC, note that "float" covers the case "NaN"
# (special Not-a-Number number) and "inf" (infinity)
print(f"REJECTED: type is purely numeric: {type(m)}")
print("---------------")
return
if type(m) == list or type(m) == tuple:
# DUBIOUS, one could convert those to Numpy array though
print(f"REJECTED: type is {type(m)} of length {len(m)}")
print("---------------")
return
elif type(m) == np.ndarray:
# GOOD: Numpy array
print(f"The type of m is: {type(m)}")
print(f"The shape of m is: {m.shape}")
print(f"The type of elements held by m is: {m.dtype}")
else:
# BAD: Might be anything
print(f"REJECTED: type is {type(m)}, very unexpected")
print("---------------")
return
# Distinguish cases by the number of "axes" of the Numpy multidimensional
# array. The number of axes is commonly called "number of dimensions",
# which is incorrect if you think about it, as this is not a dimension
# along which to measure some value but an axis along which to arrange
# independent values. In fact, each cell in a matrix corresponds to
# a dimension in and of itself. A 2x2 matrix properly has 4 dimensions!
# But - as customarily, naming is messed up and firmly entrenched.
if m.ndim == 1:
print("SUSPECT: 1-dimensional array")
print(f"The length along the unique axis is: {m.shape[0]}")
elif m.ndim == 2 and m.shape[0] == 1 and m.shape[1] == 1:
print("SPECIAL CASE: 1x1 matrix")
elif m.ndim == 2 and m.shape[1] == 1 and m.shape[0] > 1:
print(f"SPECIAL CASE: matrix shaped as single column of length {m.shape[0]}")
elif m.ndim == 2 and m.shape[0] == 1 and m.shape[1] > 1:
print(f"SPECIAL CASE: matrix shaped as single row of length {m.shape[1]}")
elif m.ndim == 2 and m.shape[0] > 1 and m.shape[0] == m.shape[1]:
print(f"SPECIAL CASE: square matrix of common length {m.shape[0]} along both axes")
else:
print(f"GENERAL CASE: matrix with {m.ndim} axes")
for axis in range(0, m.ndim):
if axis == m.ndim - 1:
alias = " (axis of columns)"
elif axis == m.ndim - 2:
alias = " (axis of rows)"
elif axis == m.ndim - 3:
alias = " (axis of layers)" # is 'layers' a good name?
else:
alias = "" # we don't know what to call even larger groups, maybe 'frame'
print(f"The length along axis {axis}{alias} is: {m.shape[axis]}")
print(str(m))
print("---------------")
# This will be rejected as a string.
examine("a")
# This will be rejected as a purely numeric 'int'.
examine(1)
# This will be rejected as a purely numeric 'complex'.
examine(1 + 2j)
# This will be rejected as a 'list' (of length 1)
examine([1])
# This will be rejected as a 'list' (of length 3)
examine([1, 2, 3])
# This will be rejected as a purely numeric int.
# This is *not* a 'tuple' of length 1, just an 'int' in parentheses.
examine((1))
# This will be rejected as a tuple (of length 1).
# Python distinguishes a "number in parentheses" from a "1-element tuple"
# by the - only apparently useless - trailing comma (a parsing ambiguity of
# the language that had to be solved somehow)
examine((1,))
# This will be rejected as a 'tuple' (of length 3)
examine((1, 2, 3))
# A 'numpy array' that just has 1 dimension (I prefer 'just has 1 axis')
examine(np.array([1]))
# Another 'numpy array' that just has 1 dimension (I prefer 'just has 1 axis')
examine(np.array([1, 2, 3]))
# A 'numpy array' that is a matrix of size 1x1 (2 dimensions, a 2D-matrix with 1 cell)
examine(np.array([[1]]))
# A 'numpy array' that is a 1x3 matrix
examine(np.array([[1, 2, 3]]))
# A 'numpy array' that is a 3x1 matrix
examine(np.array([[1], [2], [3]]))
# A 'numpy array' that is 2x2 square matrix
examine(np.array([[1, 2], [3, 4]]))
# A 'numpy array' that is a 3x5 matrix (written as 3 rows of 5 entries/columns)
examine(np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15]]))
# A 'numpy array' that is a 3x5x2 matrix (3 'layers' of 5 rows of 2 entries/columns)
# 'layer' sounds like a good word for the largest element
examine(np.array(
[[[1, 2],
[3, 4],
[5, 6],
[7, 8],
[9, 10]],
[[11, 12],
[13, 14],
[15, 16],
[17, 18],
[19, 20]],
[[21, 22],
[23, 24],
[25, 26],
[27, 28],
[29, 30]]]))
# A 'numpy array' that is a 2x3x4x5 matrix, which we build explicitly.
# We name the hierarchy: 2 tops -> 3 layers -> 4 rows -> 5 entries/columns
mx = np.zeros((2, 3, 4, 5), dtype=int)
for top_index in range(0, mx.shape[0]):
for layer_index in range(0, mx.shape[1]):
for row_index in range(0, mx.shape[2]):
for col_index in range(0, mx.shape[3]):
mx[top_index, layer_index, row_index, col_index] = (((top_index + 1) * 10 + (layer_index + 1)) * 10 + (row_index + 1)) * 10 + (col_index + 1)
examine(mx)
The above gives the following output:
REJECTED: type is <class 'str'>, very unexpected
---------------
REJECTED: type is purely numeric: <class 'int'>
---------------
REJECTED: type is purely numeric: <class 'complex'>
---------------
REJECTED: type is <class 'list'> of length 1
---------------
REJECTED: type is <class 'list'> of length 3
---------------
REJECTED: type is purely numeric: <class 'int'>
---------------
REJECTED: type is <class 'tuple'> of length 1
---------------
REJECTED: type is <class 'tuple'> of length 3
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (1,)
The type of elements held by m is: int64
SUSPECT: 1-dimensional array
The length along the unique axis is: 1
[1]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (3,)
The type of elements held by m is: int64
SUSPECT: 1-dimensional array
The length along the unique axis is: 3
[1 2 3]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (1, 1)
The type of elements held by m is: int64
SPECIAL CASE: 1x1 matrix
[[1]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (1, 3)
The type of elements held by m is: int64
SPECIAL CASE: matrix shaped as single row of length 3
[[1 2 3]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (3, 1)
The type of elements held by m is: int64
SPECIAL CASE: matrix shaped as single column of length 3
[[1]
[2]
[3]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (2, 2)
The type of elements held by m is: int64
SPECIAL CASE: square matrix of common length 2 along both axes
[[1 2]
[3 4]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (3, 5)
The type of elements held by m is: int64
GENERAL CASE: matrix with 2 axes
The length along axis 0 (axis of rows) is: 3
The length along axis 1 (axis of columns) is: 5
[[ 1 2 3 4 5]
[ 6 7 8 9 10]
[11 12 13 14 15]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (3, 5, 2)
The type of elements held by m is: int64
GENERAL CASE: matrix with 3 axes
The length along axis 0 (axis of layers) is: 3
The length along axis 1 (axis of rows) is: 5
The length along axis 2 (axis of columns) is: 2
[[[ 1 2]
[ 3 4]
[ 5 6]
[ 7 8]
[ 9 10]]
[[11 12]
[13 14]
[15 16]
[17 18]
[19 20]]
[[21 22]
[23 24]
[25 26]
[27 28]
[29 30]]]
---------------
The type of m is: <class 'numpy.ndarray'>
The shape of m is: (2, 3, 4, 5)
The type of elements held by m is: int64
GENERAL CASE: matrix with 4 axes
The length along axis 0 is: 2
The length along axis 1 (axis of layers) is: 3
The length along axis 2 (axis of rows) is: 4
The length along axis 3 (axis of columns) is: 5
[[[[1111 1112 1113 1114 1115]
[1121 1122 1123 1124 1125]
[1131 1132 1133 1134 1135]
[1141 1142 1143 1144 1145]]
[[1211 1212 1213 1214 1215]
[1221 1222 1223 1224 1225]
[1231 1232 1233 1234 1235]
[1241 1242 1243 1244 1245]]
[[1311 1312 1313 1314 1315]
[1321 1322 1323 1324 1325]
[1331 1332 1333 1334 1335]
[1341 1342 1343 1344 1345]]]
[[[2111 2112 2113 2114 2115]
[2121 2122 2123 2124 2125]
[2131 2132 2133 2134 2135]
[2141 2142 2143 2144 2145]]
[[2211 2212 2213 2214 2215]
[2221 2222 2223 2224 2225]
[2231 2232 2233 2234 2235]
[2241 2242 2243 2244 2245]]
[[2311 2312 2313 2314 2315]
[2321 2322 2323 2324 2325]
[2331 2332 2333 2334 2335]
[2341 2342 2343 2344 2345]]]]
---------------