Safer 'operator.itemgetter' in Python

Redowan Delowar June 16, 2022
Source

Python's operator.itemgetter is quite versatile. It works on pretty much any iterables and map-like objects and allows you to fetch elements from them. The following snippet shows how you can use it to sort a list of tuples by the first element of the tuple:

Here, the itemgetter callable is doing the work of selecting the first element of every tuple inside the list and then the sorted function is using those values to sort the elements. Also, this is faster than using a lambda function and passing that to the key parameter to do the sorting:

You can also use itemgetter to extract multiple values from a dictionary in a single pass. Consider this example:

So, instead of extracting the key-value pairs with d['foo'], d['bar'], ..., itemgetter allows us to make it DRY. The source code of the callable is freakishly simple. Here's the entire thing:

While this is all good and dandy, itemgetter will raise a KeyError if it can't find the corresponding value against a key in a map or an IndexError if the provided index is outside of the range of the sequence. This is how it looks in a dict:

In the above snippet, itemgetter can't find the key fiz in the dict d and it complains when we try to fetch the value against it. In a sequence, the error looks like this:

A more tolerant version of 'operator.itemgetter'

I wanted something that works similar to itemgetter but doesn't raise these exceptions when it can't find the key in a dict or the index of a sequence is out of range. Instead, it'd return a default value when these exceptions occur. So, to avoid KeyError in a map, it'd use d.get(key, default) instead of d[key] to fetch the value. Similarly, in a sequence, it'd first compare the length of the sequence with the index and return a default value if the index is out of range.

Since operator.itemgetter is a class, we could inherit it and overwrite the init method. However, your type-checker will complain if you do so. That's because, in the stub file, the itemgetter class is decorated with the typing.final decorator and isn't meant to be subclassed. So, our only option is to rewrite it. The good news is that this implementation is quite terse just like the original. Here it goes:

This class behaves almost the same way as the original itemgetter function. The only difference is that you can pass a default value to return instead of raising KeyError/IndexError depending on the type of the container. Let's try it out with a dict:

Here, we're trying to access a bunch of keys that don't exist in the dict d and we want to do this without raising any exceptions. You can see that instead of raising an exception, safe_itemgetter returns a tuple containing the value(s) that it can find and the rest of the positions are filled with the default value; in this case, the sentinel. We can pass any default value there:

This works similarly when a sequence is passed:

This returns an empty tuple when the sequence index is out of range. It works with multiple indices as well:

Further reading

Discussion in the ATmosphere

Loading comments...