ndindex generates the multidim index tuples for any array
In fact with this we can generate the nd array of dtype object, and fill it with some function of these indices in one step (ok, 2 steps):
shape=(3,4,2)
A = np.empty(shape, dtype=object)
for i in np.ndindex(3,4,2):
A[i]=i # or use A[i]=list(i)
producing A:
array([[[(0, 0, 0), (0, 0, 1)],
[(0, 1, 0), (0, 1, 1)],
[(0, 2, 0), (0, 2, 1)],
[(0, 3, 0), (0, 3, 1)]],
[[(1, 0, 0), (1, 0, 1)],
[(1, 1, 0), (1, 1, 1)],
[(1, 2, 0), (1, 2, 1)],
[(1, 3, 0), (1, 3, 1)]],
[[(2, 0, 0), (2, 0, 1)],
[(2, 1, 0), (2, 1, 1)],
[(2, 2, 0), (2, 2, 1)],
[(2, 3, 0), (2, 3, 1)]]], dtype=object)
I filled this with tuples rather than lists because the display is clearer. In
array([[[[0, 0, 0], [0, 0, 1]],
[[0, 1, 0], [0, 1, 1]],
[[0, 2, 0], [0, 2, 1]],
....
it is hard to distinguish between an array dimension and a list.
With dtype object, the individual elements could be anything. np.empty fills it with None.
A related data structure would be a flat list of these tuples. It could still be accessed as though it were nd by using np.ravel_multi_index.
L1 = [i for i in np.ndindex(shape)]
[(0, 0, 0),
(0, 0, 1),
(0, 1, 0),
...
(2, 2, 1),
(2, 3, 0),
(2, 3, 1)]
Accessed as a simulated deeply nested list:
L1[np.ravel_multi_index(i,shape)]
A few access time comparisons:
In [137]: %%timeit
for i in np.ndindex(shape):
x=A[i]
10000 loops, best of 3: 88.3 µs per loop
In [138]: %%timeit
for i in np.ndindex(shape):
x=L1[np.ravel_multi_index(i,shape)]
1000 loops, best of 3: 227 µs per loop
So multidimensional indexing of the array is faster.
In [140]: %%timeit
.....: for i in L1:
.....: x=i
1000000 loops, best of 3: 774 ns per loop
In [143]: %%timeit
.....: for i in A.flat:
x=i
1000000 loops, best of 3: 1.44 µs per loop
But direct iteration over the list is much faster.
for the itertools.produce iterator:
In [163]: %%timeit
.....: for i in itertools.product(*[range(x) for x in shape]):
x=A[i]
.....:
100000 loops, best of 3: 12.1 µs per loop