elegant map over 2D list

What's an elegant way to map func over each value in a 2D list?

The best I have so far:

result = [[func(value) for value in row] for row in grid]

It's a shame I have to nest square brackets, and have a lot of list comprehensions. Is there a less verbose or more elegant way?

result = map2d(func, grid)

Solution 1:

"It's a shame I have to nest square brackets" - No it's not, it's a Pythonic way to go and makes it clear that result is a list of lists (as long as you don't go overboard with even more nested list comprehensions). If you really don't want to see that in the code just wrap it in your own function:

def map2d(func, grid):
    return [[func(value) for value in row] for row in grid]
result = map2d(func, grid)

Solution 2:

I think your initial approach is obvious, readable, error free and short enough for the task at hand. It can be refactored to a "map2d" func, but then, just create this func and put that code in, like in: map2d= lambda func, data: [[func(element) for element in row] for row in data]

What might feel strange about nestign the brackets is that the "list inside list" approach for 2D data, although straightforward, have some drawbacks.

It is easy to have other forms of 2D data structures in Python, by implementing a new class with custom __getitem__, __setitem__ and __delitem__ methods that will give allow the syntax mydata[x, y]. And then, if you derive your class, for example, from collections.abc.Mapping, you can iterate over it, or over the return of mydata.itemsto reach all data points with a linear for loop.

another advantage is that a custom class can them know about its width X height and check boundaries - and just return a default value for unfilled elments, working as a sparse matrix.

Another approach is simply using a dictionary, with no class creation, for your 2D data, and retrieve data with the get method in order to have a default value at hand:

from random import randint
W, H = 10, 10
grid = dict()
# fill in data:
for x in range(W):
   for y in range(H):
      grid[x,y] = randint(0, 100)
# loop through all values:
for coord in grid:
     print(grid[coord])
     # change value:
     grid[coord] = new_value(grid[coord])

This approach won't let you do "map" with a onelinet, but by explictly calling in grid.__setitem__ though. By using a custom class, like suggested above, you can simply add a .map method that will do that.