You can basically assign np.arange() to an appropriate slicing of your input.
You can do this either hard-coding the 2D nature of your input (foo2()), or, for arbitrary dimensions using a dynamically defined slicing (foon()):
import numpy as np
def foo(arr):
ii, jj = arr.shape
for i in range(ii):
for j in range(jj):
if i == 0:
arr[i, j] = j
elif j == 0:
arr[i, j] = i
return arr
def foo2(arr):
ii, jj = arr.shape
arr[:, 0] = np.arange(ii)
arr[0, :] = np.arange(jj)
return arr
def foon(arr):
for i, d in enumerate(arr.shape):
slicing = tuple(slice(None) if j == i else 0 for j in range(arr.ndim))
arr[slicing] = np.arange(d)
return arr
arr = np.zeros((3, 4))
print(foo(arr))
# [[0. 1. 2. 3.]
# [1. 0. 0. 0.]
# [2. 0. 0. 0.]]
print(foo2(arr))
# [[0. 1. 2. 3.]
# [1. 0. 0. 0.]
# [2. 0. 0. 0.]]
print(foon(arr))
# [[0. 1. 2. 3.]
# [1. 0. 0. 0.]
# [2. 0. 0. 0.]]
Note that double slicing (e.g. MAT[i][j]), while working, is not as efficient as slicing using a tuple (e.g. MAT[i, j]).
Finally, the nesting of the loops is largely unused in your code, and you could rewrite it with the two loops being separated (which is much more efficient):
def fool(arr):
ii, jj = arr.shape
for i in range(ii):
arr[i, 0] = i
for j in range(jj):
arr[0, j] = j
return arr
This is interesting because if we accelerate the code using Numba:
fool_nb = nb.jit(fool)
fool_nb.__name__ = 'fool_nb'
This results in the fastest approach:
funcs = foo, foo2, foon, fool, fool_nb
shape = 300, 400
for func in funcs:
arr = np.zeros(shape)
print(func.__name__)
%timeit func(arr)
print()
# foo
# 100 loops, best of 3: 6.53 ms per loop
# foo2
# 100000 loops, best of 3: 4.28 µs per loop
# foon
# 100000 loops, best of 3: 6.99 µs per loop
# fool
# 10000 loops, best of 3: 89.8 µs per loop
# fool_nb
# 1000000 loops, best of 3: 1.01 µs per loop