3

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?

TheSoundDefense
  • 6,000
  • 1
  • 24
  • 39
Carst
  • 1,554
  • 2
  • 15
  • 28
  • You could adjust `div/mul/add` to be a more generic `reduce_list(list_of_arguments, op)` and pass in `operator.add` & co. as the `op`... – Jon Clements Aug 18 '14 at 19:53
  • Functional style would improve readability but function calling being so expensive in Python... – Paulo Scardine Aug 18 '14 at 19:59
  • Why can't you eliminate all exceptions? `TypeError` exceptions via `isinstance` as described [here](http://stackoverflow.com/a/3441601/3903832) and `ZeroDivisionError` exceptions by setting the results of calculations that divide by 0 to `None` without trying to perform the actual calculations. – Yoel Aug 18 '14 at 20:11
  • Hi Yoel, the problem is that I do not think it would improve readability or functional understanding but instead create a wealth of "if then elif then else" statements for each calculation. See also: http://stackoverflow.com/questions/7604636/better-to-try-something-and-catch-the-exception-or-test-if-its-possible-first – Carst Aug 18 '14 at 20:23

3 Answers3

6

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.)

Kevin
  • 69,817
  • 12
  • 97
  • 139
  • That is actually a really nice solution and very high performant (in between the current solution and option 1)! – Carst Aug 18 '14 at 20:02
  • Hmmm cannot edit my previous comment. About the order, it matters, but we could solve that by making it a list of tuples: [('10', lambda: float(a['1'] * a['2'])), ('11', etc.), etc.] – Carst Aug 18 '14 at 20:24
  • 1
    @Carst just to note - you can only edit comments within 5 minutes of them being posted :) – Jon Clements Aug 18 '14 at 20:27
3

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...
Jon Clements
  • 124,071
  • 31
  • 219
  • 256
  • Also really nice and definitely a very readable solution; I think I prefer your first solution over the second one because the "(a['1'] * a['2']) / (a['3'] * a['4'])" example will still be completely readable in the first and might be a bit more of a headache to understand. Thanks for the input, really interesting ways to handle this! – Carst Aug 18 '14 at 20:08
1

I have two solutions one of them slightly faster then the other.

More readable:

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

    fn_map = {
        '*': operator.mul,
        '/': operator.div,
    }

    def calc(x, fn, y):
        try:
            return float(fn_map[fn](a[x], a[y]))
        except (ZeroDivisionError, KeyError, TypeError, ValueError):
            return None

    a['10'] = calc('1', '*', '2')
    a['11'] = calc('1', '*', '5')
    a['12'] = calc('1', '*', '6')
    a['13'] = calc('1', '/', '2')
    a['14'] = calc('1', '/', '3')
    a['15'] = calc(calc('1', '*', '2', '/', calc('3', '*', '4'))
    return a

Slightly faster:

from operator import mul, div

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

    def calc(x, fn, y):
        try:
            return float(fn(a[x], a[y]))
        except (ZeroDivisionError, KeyError, TypeError, ValueError):
            return None

    a['10'] = calc('1', mul, '2')
    a['11'] = calc('1', mul, '5')
    a['12'] = calc('1', mul, '6')
    a['13'] = calc('1', div, '2')
    a['14'] = calc('1', div, '3')
    a['15'] = calc(calc('1', mul, '2', div, calc('3', mul, '4'))
    return a
VStoykov
  • 1,445
  • 10
  • 14