Roman
Roman

Reputation: 2365

Access multidimensional list value by tuple of indices in python

I have a complex multidimensional utterable object (say, list). I want to write function to access the value of an element represented by tuple of indices. The number of dimensions is itself variable. It should also return None if the element does not exist:

l = [[[1, 2],
      [3, 4]],
     [[5, 6],
      [7, 8]]]
access(l, (0, 0, 0)) # prints 1
access(l, (0, 1, 1)) # prints 4
access(l, (1, 1)) # prints [7, 8]
access(l, (0, 1, 2)) # prints None
access(l, (0, 0, 0, 0)) # prints None

How do I achieve this?

Upvotes: 2

Views: 2333

Answers (4)

thayne
thayne

Reputation: 1048

The prior solutions helped me a lot, but I also needed to change the value at the index of the tuple. For example: access(l, (0,0,0)) = 7.

The other solutions return a primitive value and so the nested list doesn't get updated.

I found that this approach, based on previous answers works (in case someone has the same need as me):

def set_it(obj, indexes, v):
   a = obj
   for i in indexes[:-1]:
     a = a[i]
   a[indexes[-1]] = v

Usage:

>>> l = [[[1, 2],
...       [3, 4]],
...      [[5, 6],
...       [7, 8]]]
>>> set_it(l, (0, 0, 0), -1)
>>> l
[[[-1, 2], [3, 4]], [[5, 6], [7, 8]]]

Upvotes: 0

nigel222
nigel222

Reputation: 8212

It's not a great deal harder to do as chepner suggests, but a bit more carefully. Return None only if the exception is IndexError and what is tried is more specific. Something like

def access(obj, indexes):
    a = obj
    for i in indexes:
       try: 
           a = a[i]
       except IndexError:
           return None
       # except TypeError:
           # when you try to index deeper than the object supports

    # a  is not constrained to be a scalar, it may still be dimensional
    # if insufficient indexes were passed.
    return a  

BTW this will work if at some levels you have a dict not a list. ie

x = access(obj, (3, 'default', 'range', 1))

will work for an appropriate object, will throw an exception if one or two levels down is not a dict or doesn't have the key. I've used something very like this for accessing JSON.load results. You might want also to return None on KeyError which is the dict equivalent of IndexError.

You add tests such as isinstance(a, list) or hasattr(a, '__iter__') to make even more sure that you are indexing what was intended.

Upvotes: 2

chepner
chepner

Reputation: 531285

An alternate approach to Yaroslav's answer, in which you ask forgiveness rather than permission before indexing the list:

def access(obj, indexes):
    try:
        return reduce(list.__getitem__, indexes, obj)
    except Exception:
        return None

It's not as foolproof; it make the assumption that access will be used as intended, and you don't mind getting None back if someone decides to try access(1, 1), for example.

Upvotes: 2

Yaroslav Admin
Yaroslav Admin

Reputation: 14535

You can do it using reduce():

def access(obj, indexes):
    return reduce(lambda subobj, index: subobj[index] if isinstance(subobj, list) and index < len(subobj) else None, indexes, obj)

Or as pointed by @chepner, you can use def to make it more readable:

def access(obj, indexes):
    def _get_item(subobj, index): 
        if isinstance(subobj, list) and index < len(subobj):
            return subobj[index]
        return None

    return reduce(_get_item, indexes, obj)

Upvotes: 1

Related Questions