2

dicts have a convenient version of the get:

get(key[, default])

Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method never raises a KeyError.

Bold emphasis mine, because I looked and could not find an equivalent version for lists. So I implemented my own:

In [148]: class myList(list):
     ...:     def pop(self, idx=-1, default=None):
     ...:         try:
     ...:             return super().pop(idx)
     ...:         except IndexError:
     ...:             return default

This works as expected:

In [149]: l = myList([1, 2, 3, 4, 5])

In [150]: l.pop()
Out[150]: 5

In [151]: l.pop(12345, 'default')
Out[151]: 'default'

Since this works, this can also be extended to sets. Anyway, I've got a couple of questions:

  1. Is there an easier/inbuilt/3rd party equivalent to what I've done that doesn't require me to extend the list class?

  2. If there isn't, is there any particular reason why? I believe this would be a useful behaviour to have.

Some particular use cases I can think of would be calling pop on a list whose size you don't know, in a place you don't want to, or can't catch errors, such as a list comp.


This question explores the same topic but the answers do not suit my requirement, as I have explained above.

cs95
  • 379,657
  • 97
  • 704
  • 746

2 Answers2

3

One reason why it is only implemented for dictionaries is that checking if key in some_dict isn't always an O(1) operation in the worst case it could be O(n). But even if the lookup is O(1) it's actually an expensive operation because you need to hash the key, then if there's a match you need to compare the key to the stored key for equality. That makes LBYL very expensive for dictionaries.

For lists on the other hand checking that the index is within bounds is a quite inexpensive operation. I mean you could check if it's within bounds with a simple:

valid_index = -len(some_list) <= index < len(some_list)

But that's just for LBYL approaches. One could always use EAFP and catch the Exception.

That should be impose the same overhead to lists and dicts. So why has dict a get method to avoid exception handling? The reason is actually quite simple: dicts are the fundamental building blocks of almost everything. Most classes and all modules are essentially just dictionaries and a lot of user code actually uses dictionaries. It was (and still is) just worth it to have a method that can do a guaranteed return without the exception handling overhead.

For lists it could be useful too (I'm not so sure on that point) but I think in most cases when it throws an IndexError it happened accidentally, not on purpose. So it would hide a real "bug" if it would return Nones.

That's just my reasoning about this. I could be that the Python developers had completely different reasons for the current behavior.


Regarding your first question:

Is there an easier/inbuilt/3rd party equivalent to what I've done that doesn't require me to extend the list class?

Also I don't know a standard library or 3rd party module for these operations with defaults for lists or sets. Some libraries implement a "sparse list" (a list filled with a default value) but those I've looked at don't handle the "empty list" case and they use one default for the complete list.

However there could be some options that might be interesting in case you don't want to do the "exception handling" yourself:

  • The iter_except recipe from the itertools documentation page which stops to call a function when a specified exception occurs:

    def iter_except(func, exception, first=None):
        # Copied verbatim from the above mentioned documentation page
        try:
            if first is not None:
                yield first()
            while True:
                yield func()
        except exception:
            pass
    
    >>> l = [1,2,3,4]
    >>> list(iter_except(l.pop, IndexError))
    [4, 3, 2, 1]
    
  • contextlib.suppress

    import contextlib
    def pop_with_default(lst, ind=-1, default=None):
        with contextlib.suppress(IndexError):
            return lst.pop(ind)
        return default
    
    >>> pop_with_default([1,2,3], 5, 'abc')
    'abc'
    

Another way to deal with the general problem here is to create a function that calls another function and in case of a specified exception will return a default value otherwise the result of the function call:

def call_with_default(func, default=None, *exceptions):
    def inner(*args, **kwargs):
        try:
            res = func(*args, **kwargs)
        except exceptions:
            res = default
        return res
    return inner

>>> a = [1,2,3,4]
>>> a_pop_with_default = call_with_default(a.pop, 'abc', IndexError)
>>> [a_pop_with_default(2) for _ in range(10)]
[3, 4, 'abc', 'abc', 'abc', 'abc', 'abc', 'abc', 'abc', 'abc']
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 2
    https://mail.python.org/pipermail/python-dev/1999-July/000580.html and https://groups.google.com/forum/#!msg/comp.lang.python/6xRvGCCrYXs/iDZi4MZcMboJ – Ashwini Chaudhary Aug 08 '17 at 07:59
  • 1
    @cᴏʟᴅsᴘᴇᴇᴅ I only found some libs implementing a sparse list but these use a global default and don't handle the case when the list is empty and you want to `pop` an item. But there is one thing that could be interesting in case you want to stop a comprehension when an exception is raised `iter_except` ([`itertools` "recipes" section](https://docs.python.org/library/itertools.html#itertools-recipes)). Likewise there is [`contextlib.suppress`](https://docs.python.org/library/contextlib.html#contextlib.suppress). But these don't handle the "default" case so they may not be appropriate here. – MSeifert Aug 08 '17 at 08:33
1

Why couldn't you use something simple like this?

my_list.pop(n) if n < len(my_list) else "default"

EDIT

Good comments. What are your thoughts about something like this?

def list_popper(my_list, idx=-1, default='default'):
    try:
        default = my_list.pop(idx)
    except IndexError:
        return default

The functionality is the same as the OP's class inheritance method. For the sake of readability and future maintenance, however, I generally prefer a simple function over the creation of mundane classes.

Alexander
  • 105,104
  • 32
  • 201
  • 196