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?