python map exception continue mapping execution

Question:

The following example is very simple. I want to execute map() with a function which can raise Exception. It will be more clearly with an example :

number_list = range(-2,8)

def one_divide_by(n):
    return 1/n

try:
    for number, divide in zip(number_list, map(one_divide_by, number_list)):
        print("%d : %f" % (number, divide))
except ZeroDivisionError:
    # Execution is stopped. I want to continue mapping
    pass

When I execute this code I get :

-2 : -0.500000
-1 : -1.000000

It’s due to the 0 in my list. I don’t want remove this 0 (because in real case I can’t know first if I will get Exception).
Do you know how to continue mapping after the exception ?

Asked By: Samuel Dauzon

||

Answers:

you could catch the exception in your function (instead of in the for loop) and return None (or whatever you choose) if ZeroDivisionError is raised:

def one_divide_by(n):
    try:
        return 1/n
    except ZeroDivisionError:
        return None

if you choose to return None you need to adapt your format string; None can not be formatted with %f.

other values you could return (and that would be compatible with your string formatting) are float('inf') (or float('-inf') depending on the sign of your numerator) or float('nan') – “infinity” or “not a number”.

here you will find some of the caveats of using float('inf').

Answered By: hiro protagonist

You can move the try/except block inside the function. Example –

def one_divide_by(n):
    try:
        return 1/n
    except ZeroDivisionError:
        return 0   #or some other default value.

And then call this normally, without a try/except block –

for number, divide in zip(number_list, map(one_divide_by, number_list)):
    print("%d : %f" % (number, divide))
Answered By: Anand S Kumar

If you can replace map instead of changing the definition of your function one_divide_by, I would recommend more_itertools.map_except:

from more_itertools import map_except

number_list = range(-2,8)

def one_divide_by(n):
    return 1/n

for number, divide in map_except(
        lambda number: (number, one_divide_by(number)),
        number_list,
        ZeroDivisionError
):
    print(number, ':', divide)

This prints, as expected:

-2 : -0.5
-1 : -1.0
1 : 1.0
2 : 0.5
3 : 0.3333333333333333
4 : 0.25
5 : 0.2
6 : 0.16666666666666666
7 : 0.14285714285714285

The function more_itertools.map_except accepts a function, an iterable as inputs (just like the built-in map) and expected exceptions. When one of those exceptions is raised during the mapping process, the element is simply skipped.

This package can be installed using pip install more-itertools, for pip users, or conda install -c conda-forge more-itertools, for Conda users.

Answered By: ruancomelli

Another option is to define a helper function that accepts a handler for exceptions like this:

def map_e(function, *iterables, on_exception=None):
    for elements in zip(*iterables):
        try:
            yield function(*elements)
        except Exception as e:
            yield on_exception(elements, exception=e) 
                if callable(*on_exception) 
                else None


list_a = [1,  2, 5, 1]
list_b = [2, -2, 6, 6]
list_c = [2,  1, 0, 8]


def sum_then_divide(a, b, c):
    return (a + b) / c


def none_on_exception(*args, **kwargs):
    return None


result = list(
    map_e(
        sum_then_divide,
        list_a,
        list_b,
        list_c,
        on_exception=none_on_exception
    )
)

print(result)

No need to handle the exception in your main function, and no need to add external libraries.

Answered By: UselesssCat

As ruancomelli pointed out, map_except can drop the offending function call instance from your resulting iterable. This may not be your the desired behaviour.

If you want the offending function call to still appear in the output stream, you can use function composition to transform the exception into something else. For instance, this wrapper:

def zero_div_protect(func):
    def wrapper(*args):
        try:
            return func(*args)
        except ZeroDivisionError:
            return float('inf')
    return wrapper

will take a function (such as one_divide_by) and return a new function, wrapping the original function with a try:...except: that returns an "exceptional value" when the exception occurs.

number_list = range(-2, 8)

def one_divide_by(n):
    return 1/n

for number, divide in zip(number_list, map(zero_div_protect(one_divide_by), number_list)):
    print("%d : %f" % (number, divide))

Output:

-2 : -0.500000
-1 : -1.000000
0 : inf
1 : 1.000000
2 : 0.500000
3 : 0.333333
4 : 0.250000
5 : 0.200000
6 : 0.166667
7 : 0.142857

You can also use zero_div_protect as a "decorator", which replaces the original function with the wrapped version:

number_list = range(-2,8)

@zero_div_protect
def one_divide_by(n):
    return 1/n

for number, divide in zip(number_list, map(one_divide_by, number_list)):
    print("%d : %f" % (number, divide))

The output is the same.


See also: functools.wraps for more advanced usages.

Answered By: AJNeufeld