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: dict
s 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 None
s.
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']