Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Readable try except handling with calculations

Tags:

python

I have a program where I have quite a lot of calculations I need to do, but where the input can be incomplete (so we cannot always calculate all results), which in itself is fine, but gives issues with the readability of the code:

def try_calc():
    a = {'1': 100, '2': 200, '3': 0, '4': -1, '5': None, '6': 'a'}
    try:
        a['10'] = float(a['1'] * a['2'])
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['10'] = None
    try:
        a['11'] = float(a['1'] * a['5'])
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['11'] = None
    try:
        a['12'] = float(a['1'] * a['6'])
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['12'] = None
    try:
        a['13'] = float(a['1'] / a['2'])
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['13'] = None
    try:
        a['14'] = float(a['1'] / a['3'])
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['14'] = None
    try:
        a['15'] = float((a['1'] * a['2']) / (a['3'] * a['4']))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        a['15'] = None
    return a

In [39]: %timeit try_calc()
100000 loops, best of 3: 11 µs per loop

So this works well, is high performing but is really unreadable. We came up with two other methods to handle this. 1: Use specialized functions that handle issues internally

import operator
def div(list_of_arguments):
    try:
        result = float(reduce(operator.div, list_of_arguments, 1))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        result = None
    return result

def mul(list_of_arguments):
    try:
        result = float(reduce(operator.mul, list_of_arguments, 1))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        result = None
    return result

def add(list_of_arguments):
    try:
        result = float(reduce(operator.add, list_of_arguments, 1))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        result = None
    return result

def try_calc2():
    a = {'1': 100, '2': 200, '3': 0, '4': -1, '5': None, '6': 'a'}
    a['10'] = mul([a['1'], a['2']])
    a['11'] = mul([a['1'], a['5']])
    a['12'] = mul([a['1'], a['6']])
    a['13'] = div([a['1'], a['2']])
    a['14'] = div([a['1'], a['3']])
    a['15'] = div([
        mul([a['1'], a['2']]), 
        mul([a['3'], a['4']])
        ])
    return a

In [40]: %timeit try_calc2()
10000 loops, best of 3: 20.3 µs per loop

Twice as slow and still not that readable to be honest. Option 2: encapsulate inside eval statements

def eval_catcher(term):
    try:
        result = float(eval(term))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        result = None
    return result

def try_calc3():
    a = {'1': 100, '2': 200, '3': 0, '4': -1, '5': None, '6': 'a'}
    a['10'] = eval_catcher("a['1'] * a['2']")
    a['11'] = eval_catcher("a['1'] * a['5']")
    a['12'] = eval_catcher("a['1'] * a['6']")
    a['13'] = eval_catcher("a['1'] / a['2']")
    a['14'] = eval_catcher("a['1'] / a['3']")
    a['15'] = eval_catcher("(a['1'] * a['2']) / (a['3'] * a['4'])")
    return a

In [41]: %timeit try_calc3()
10000 loops, best of 3: 130 µs per loop

So very slow (compared to the other alternatives that is), but at the same time the most readable one. I am aware that some of the issues (KeyError, ValueError) could be also handled by pre-processing the dictionary to ensure the availability of keys but that would still leave None (TypeError) and ZeroDivisionErrors anyway, so I do not see any advantage there

My question(s): - Am I missing other options? - Am I completely crazy for trying to solve it this way? - Is there a more pythonic approach? - What do you think is the best solution to this and why?

like image 832
Carst Avatar asked Mar 15 '26 12:03

Carst


2 Answers

How about storing your calculations as lambdas? Then you can loop through all of them, only using a single try-except block.

def try_calc():
    a = {'1': 100, '2': 200, '3': 0, '4': -1, '5': None, '6': 'a'}
    calculations = {
        '10': lambda: float(a['1'] * a['2']),
        '11': lambda: float(a['1'] * a['5']),
        '12': lambda: float(a['1'] * a['6']),
        '13': lambda: float(a['1'] / a['2']),
        '14': lambda: float(a['1'] / a['3']),
        '15': lambda: float((a['1'] * a['2']) / (a['3'] * a['4']))
    }
    for key, calculation in calculations.iteritems():
        try:
            a[key] = calculation()
        except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
            a[key] = None

By the way, I don't recommend doing this if the order of the calculations matter, like if you had this in your original code:

a['3'] = float(a['1'] * a['2'])
a['5'] = float(a['3'] * a['4'])

Since dicts are unordered, you don't have any guarantee that the first equation will execute before the second. So a['5'] might be calculated using the new value of a['3'], or it might use the old value. (This isn't an issue with the calculations in the question, since the keys one through six are never assigned to, and keys 10 through 15 are never used in a calculation.)

like image 128
Kevin Avatar answered Mar 18 '26 00:03

Kevin


A slight variation from Kevin's in that you don't need to pre-store the calculations, but instead use a decorator and lambdas to handle the errors, eg:

from functools import wraps

def catcher(f):
    @wraps
    def wrapper(*args):
        try:
            return f(*args)
        except (ZeroDivisionError, KeyError, TypeError, ValueError):
            return None
    return wrapper

a = {'1': 100, '2': 200, '3': 0, '4': -1, '5': None, '6': 'a'}

print catcher(lambda: a['1'] * a['5'])()

And as I mentioned in comments, you could also make generic your 2nd example:

import operator

def reduce_op(list_of_arguments, op):
    try:
        result = float(reduce(op, list_of_arguments, 1))
    except (ZeroDivisionError, KeyError, TypeError, ValueError) as e:
        result = None
    return result

a['10'] = do_op([a['1'], a['2']], operator.mul) 
# etc...
like image 30
Jon Clements Avatar answered Mar 18 '26 01:03

Jon Clements



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!