Skip to content Skip to sidebar Skip to footer

Rpn Evaluator Optimization Without Losing Readability

I'm implementing a program that needs a RPN calculator function, I've got the one bellow, but being new to python I wonder if I can optimize it without losing the readability. Foun

Solution 1:

The original implementation is fine, and clear. Here's another that uses more Pythonic features:

  • tests using py.test (<3)

  • parse errors are left alone, as they already indicate what's going on

  • the operator module is used directly for many two-argument functions, like multiply

  • likewise, single-argument math functions like sin/cos just call the math library

  • for convenience, the expression can be specified as a single string, like "2 3 /"

Calculators are a lot of fun, and are a great introduction to cool topics like compiling and parsing. Have fun!

RPN calculator

import math
import operator

import pytest


ERROR_VALUE = -1.defsafe_divide(darg1, darg2):
    try:
        return darg1/darg2
    except ZeroDivisionError:
        return ERROR_VALUE

defparseRPN(expression):
    """Parses and calculates the result of a RPN expression
        takes a list in the form of ['2','2','*']
        returns 4
    """# allow simple string: "2 3 /"ifisinstance(expression, basestring):
        expression = expression.split()

    function_twoargs = {
    '*': operator.mul,
    '/': safe_divide,
    '+': operator.add,
    '-': operator.sub,
    }
    function_onearg = {
    'sin': math.sin,
    'cos': math.cos,
    }
    stack = []
    for val in expression:
        result = Noneif val in function_twoargs:
            arg2 = stack.pop()
            arg1 = stack.pop()
            result = function_twoargs[val](arg1, arg2)
        elif val in function_onearg:
            arg = stack.pop()
            result = function_onearg[val](arg)
        else:
            result = float(val)
        stack.append(result)
    return stack.pop()


deftest_happy_paths():
    assert parseRPN([2, 3, '*']) == 6.assert parseRPN('0 sin') == 0.assert parseRPN([2, 3, '/']) == 2./3deftest_safe_divide():
    assert parseRPN([2, 0, '/']) == ERROR_VALUE

deftest_parse_error():
    with pytest.raises(ValueError) as excinfo:
        parseRPN('gin')
    assertstr(excinfo.value) == 'could not convert string to float: gin'

Solution 2:

This is my version:

import operator
import math

_add, _sub, _mul = operator.add, operator.sub, operator.mul
_truediv, _pow, _sqrt = operator.truediv, operator.pow, math.sqrt
_sin, _cos, _tan, _radians = math.sin, math.cos, math.tan, math.radians
_asin, _acos, _atan = math.asin, math.acos, math.atan
_degrees, _log, _log10 = math.degrees, math.log, math.log10
_e, _pi = math.e, math.pi
_ops = {'+': (2, _add), '-': (2, _sub), '*': (2, _mul), '/': (2, _truediv),
        '**': (2, _pow), 'sin': (1, _sin), 'cos': (1, _cos), 'tan': (1, _tan),
        'asin': (1, _asin), 'acos': (1, _acos), 'atan': (1, _atan),
        'sqrt': (1, _sqrt), 'rad': (1, _radians), 'deg': (1, _degrees),
        'ln': (1, _log), 'log': (1, _log10)}
_okeys = tuple(_ops.keys())
_consts = {'e': _e, 'pi': _pi}
_ckeys = tuple(_consts.keys())


defpostfix(expression):
    """
    Evaluate a postfix expression.

    Arguments:
        expression: The expression to evaluate. Should be a string or a
                    sequence of strings. In a string numbers and operators
                    should be separated by whitespace

    Returns:
        The result of the expression.
    """ifisinstance(expression, str):
        expression = expression.split()
    stack = []
    for val in expression:
        if val in _okeys:
            n, op = _ops[val]
            if n > len(stack):
                raise ValueError('not enough data on the stack')
            args = stack[-n:]
            stack[-n:] = [op(*args)]
        elif val in _ckeys:
            stack.append(_consts[val])
        else:
            stack.append(float(val))
    return stack[-1]

In the first five lines after the imports I make local names for the operators and constants as an optimization so we don't have to do a module lookup every time we use one. Allowing constants like e and pi is an extra feature.

The ops dictionary is key to the working of the calculator. It links the symbol of an operator to a tuple containing the number of arguments that the operator consumes and the function to call. Because of this data structure, operators do not have to be handled specially by their number of arguments.

Furthermore we save the keys for the _ops and _consts dicts in tuples, since we'll be using these a lot.

The line stack[-n:] = [op(*args)] is the heart of the calculator. It contains two tricks. First it does argument unpacking using the * operator. Second it replaces multiple values on the stack with the result of op.

Intentionally, this function does not catch exceptions caused by errors in the input data.

Post a Comment for "Rpn Evaluator Optimization Without Losing Readability"