Mikhail M.
Mikhail M.

Reputation: 5988

Why doesn't list have safe "get" method like dictionary?

Why doesn't list have a safe "get" method like dictionary?

>>> d = {'a':'b'}
>>> d['a']
'b'
>>> d['c']
KeyError: 'c'
>>> d.get('c', 'fail')
'fail'

>>> l = [1]
>>> l[10]
IndexError: list index out of range

Upvotes: 424

Views: 311341

Answers (14)

sglbl
sglbl

Reputation: 657

Credit to brandon's comment:

def safe_get(lst: list, i: int):
    return (lst[i:] or [None])[0]

mylist = [0, 1, 2]
print(safe_get(mylist, i=3))  # None
print(safe_get(mylist, i=-1)) # 2

Upvotes: 0

qräbnö
qräbnö

Reputation: 3031

Credits to jose.angel.jimenez, Gus Bus and Marek R


For the "oneliner" fans…


If you want the first element of a list or if you want a default value if the list is empty try:

liste = ['a', 'b', 'c']
value = (liste[0:1] or ('default',))[0]
print(value)

returns a

and

liste = []
value = (liste[0:1] or ('default',))[0]
print(value)

returns default


Examples for other elements…

liste = ['a', 'b', 'c']
print(liste[0:1])  # returns ['a']
print(liste[1:2])  # returns ['b']
print(liste[2:3])  # returns ['c']
print(liste[3:4])  # returns []

With default fallback…

liste = ['a', 'b', 'c']
print((liste[0:1] or ('default',))[0])  # returns a
print((liste[1:2] or ('default',))[0])  # returns b
print((liste[2:3] or ('default',))[0])  # returns c
print((liste[3:4] or ('default',))[0])  # returns default

Possibly shorter:

liste = ['a', 'b', 'c']
value, = liste[:1] or ('default',)
print(value)  # returns a

It looks like you need the comma before the equal sign, the equal sign and the latter parenthesis.


More general:

liste = ['a', None, 'c']
f = lambda l, x, d='default': l[x] if -len(l) <= x < len(l) else d
print(f(liste, 0))   # returns a
print(f(liste, 1))   # returns None
print(f(liste, 2))   # returns c
print(f(liste, 3))   # returns default
print(f(liste, -1))  # returns c

More general with falsy management:

liste = ['a', None, 'c']
f = lambda l, x, d='default': l[x] if -len(l) <= x < len(l) and l[x] not in (None, "", 0, False, [], {}, (), set(), frozenset(), b'', bytearray(b'')) else d
print(f(liste, 0))   # returns a
print(f(liste, 1))   # returns default
print(f(liste, 2))   # returns c
print(f(liste, 3))   # returns default
print(f(liste, -1))  # returns c

Tested with Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)

Upvotes: 32

Keith
Keith

Reputation: 43064

Probably because it just didn't make much sense for list semantics. However, you can easily create your own by subclassing.

class safelist(list):
    def get(self, index, default=None):
        try:
            return self[index]
        except IndexError:
            return default

def _test():
    l = safelist(range(10))
    print l.get(20, "oops")

if __name__ == "__main__":
    _test()

Upvotes: 76

arantius
arantius

Reputation: 1801

This isn't an extremely general-purpose solution, but I had a case where I expected a list of length 3 to 5 (with a guarding if), and I was breaking out the values to named variables. A simple and concise way I found for this involved:

foo = (argv + [None, None])[3]
bar = (argv + [None, None])[4]

Now foo and bar are either the 4th and 5th values in the list, or None if there weren't that many values.

Upvotes: 4

Gus Bus
Gus Bus

Reputation: 659

If you

  1. want a one liner,
  2. prefer not having try / except in your happy code path where you needn't, and
  3. want the default value to be optional,

you can use this:

list_get = lambda l, x, d=None: d if not l[x:x+1] else l[x]

Usage looks like:

>>> list_get(['foo'], 4) == None
True
>>> list_get(['hootenanny'], 4, 'ho down!')
'ho down!'
>>> list_get([''], 0)
''

Upvotes: 2

fab
fab

Reputation: 405

A reasonable thing you can do is to convert the list into a dict and then access it with the get method:

>>> my_list = ['a', 'b', 'c', 'd', 'e']
>>> my_dict = dict(enumerate(my_list))
>>> print my_dict
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
>>> my_dict.get(2)
'c'
>>> my_dict.get(10, 'N/A')

Upvotes: 24

Gordon Wrigley
Gordon Wrigley

Reputation: 11785

For small index values you can implement

my_list.get(index, default)

as

(my_list + [default] * (index + 1))[index]

If you know in advance what index is then this can be simplified, for example if you knew it was 1 then you could do

(my_list + [default, default])[index]

Because lists are forward packed the only fail case we need to worry about is running off the end of the list. This approach pads the end of the list with enough defaults to guarantee that index is covered.

Upvotes: 2

radtek
radtek

Reputation: 36370

So I did some more research into this and it turns out there isn't anything specific for this. I got excited when I found list.index(value), it returns the index of a specified item, but there isn't anything for getting the value at a specific index. So if you don't want to use the safe_list_get solution which I think is pretty good. Here are some 1 liner if statements that can get the job done for you depending on the scenario:

>>> x = [1, 2, 3]
>>> el = x[4] if len(x) > 4 else 'No'
>>> el
'No'

You can also use None instead of 'No', which makes more sense.:

>>> x = [1, 2, 3]
>>> i = 2
>>> el_i = x[i] if len(x) == i+1 else None

Also if you want to just get the first or last item in the list, this works

end_el = x[-1] if x else None

You can also make these into functions but I still liked the IndexError exception solution. I experimented with a dummied down version of the safe_list_get solution and made it a bit simpler (no default):

def list_get(l, i):
    try:
        return l[i]
    except IndexError:
        return None

Haven't benchmarked to see what is fastest.

Upvotes: 9

Vsevolod Kulaga
Vsevolod Kulaga

Reputation: 676

Try this:

>>> i = 3
>>> a = [1, 2, 3, 4]
>>> next(iter(a[i:]), 'fail')
4
>>> next(iter(a[i + 1:]), 'fail')
'fail'

Upvotes: 23

YOU
YOU

Reputation: 123927

Instead of using .get, using like this should be ok for lists. Just a usage difference.

>>> l = [1]
>>> l[10] if 10 < len(l) else 'fail'
'fail'

Upvotes: 51

Jake
Jake

Reputation: 13151

This works if you want the first element, like my_list.get(0)

>>> my_list = [1,2,3]
>>> next(iter(my_list), 'fail')
1
>>> my_list = []
>>> next(iter(my_list), 'fail')
'fail'

I know it's not exactly what you asked for but it might help others.

Upvotes: 136

Lennart Regebro
Lennart Regebro

Reputation: 172407

Your usecase is basically only relevant for when doing arrays and matrixes of a fixed length, so that you know how long they are before hand. In that case you typically also create them before hand filling them up with None or 0, so that in fact any index you will use already exists.

You could say this: I need .get() on dictionaries quite often. After ten years as a full time programmer I don't think I have ever needed it on a list. :)

Upvotes: -2

Nick Bastin
Nick Bastin

Reputation: 31339

Ultimately it probably doesn't have a safe .get method because a dict is an associative collection (values are associated with names) where it is inefficient to check if a key is present (and return its value) without throwing an exception, while it is super trivial to avoid exceptions accessing list elements (as the len method is very fast). The .get method allows you to query the value associated with a name, not directly access the 37th item in the dictionary (which would be more like what you're asking of your list).

Of course, you can easily implement this yourself:

def safe_list_get (l, idx, default):
  try:
    return l[idx]
  except IndexError:
    return default

You could even monkeypatch it onto the __builtins__.list constructor in __main__, but that would be a less pervasive change since most code doesn't use it. If you just wanted to use this with lists created by your own code you could simply subclass list and add the get method.

Upvotes: 170

Apprentice Queue
Apprentice Queue

Reputation: 2036

Dictionaries are for look ups. It makes sense to ask if an entry exists or not. Lists are usually iterated. It isn't common to ask if L[10] exists but rather if the length of L is 11.

Upvotes: 5

Related Questions