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?
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.)
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With