Your f, at least this example, works with arrays, since it uses the numpy vectorized operators + and *.
def f(i,j,k,l):
return i + 2 * j+ 3 * k + 4 * l
Using np.ix_ to generate indices that can broadcast against each other, I can produce an array that matches your iterative A:
In [14]: A1 = f(*np.ix_(np.arange(3),np.arange(4),np.arange(7),np.arange(8)))
In [15]: np.allclose(A,A1)
Out[15]: True
Now if f can work with arrays, we can't do this. A classic example is one that uses math.sin instead of np.sin. Or one that is if tests.
That ix_ way is fast, since it calls f only once. I can also generate all indices as tuples with np.ndindex, and call f many times:
In [24]: A2 = np.zeros_like(A)
In [25]: for ijk in np.ndindex(*A2.shape): A2[ijk]=f(*ijk)
In [26]: np.allclose(A,A2)
Out[26]: True
The speed will be similar to your A evaluation.
In [29]: %%timeit
...: A2 = np.zeros_like(A)
...: for ijk in np.ndindex(*A2.shape): A2[ijk]=f(*ijk)
...:
...:
758 µs ± 11.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [30]: timeit A1 = f(*np.ix_(np.arange(3),np.arange(4),np.arange(7),np.arange(8)))
54.3 µs ± 2.44 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [31]: %%timeit
...: A = np.zeros((3,4,7,8), dtype=int)
...:
...: i1 = i2 = i3 = i4 = -1
...:
...: i1 = -1
...: for r1 in A:
...: i1 += 1
...:
...: i2 = -1
...: for r2 in r1:
...: i2 += 1
....
...: A[i1, i2, i3, i4] = v
...:
506 µs ± 2.03 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Come to think of it, we can clean up your loops with a more typical Python range loop:
In [35]: %%timeit
...: A = np.zeros((3,4,7,8), dtype=int)
...: for i1 in range(A.shape[0]):
...: for i2 in range(A.shape[1]):
...: for i3 in range(A.shape[2]):
...: for i4 in range(A.shape[3]):
...: v = f(i1, i2, i3, i4)
...: A[i1, i2, i3, i4] = v
...:
343 µs ± 17.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)