0

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.

Found some solutions using dictionaries and so on, but I got lost in the 'pythonesque' parts, the lists are still somewhat of a mistery to me...

my function is :

def parseRPN(expression):
    """Parses and calculates the result fo an RPN expression
        takes a list in the form of ['2','2','*']
        returns 4
    """
    try:
        stack = []
        for val in expression:
            if val in ['-', '+', '*', '/']:
                op1 = stack.pop()
                op2 = stack.pop()
                if val=='-': result = op2 - op1
                if val=='+': result = op2 + op1
                if val=='*': result = op2 * op1
                if val=='/':
                  if op1==0:
                      result=1
                  else:
                      result = op2 / op1
                stack.append(result)
            elif val in ['sin','cos']:
                op1 =stack.pop()
                if val=='sin': result = sin(op1)
                if val == 'cos': result = cos(op1)
                stack.append(result)
            else:
                stack.append(float(val))
        return stack.pop()
    except:
        print('error parse RPN fn:parse_rpn :' + str(expression))
        return 10*10**10

Thanks in advance

spazm
  • 4,399
  • 31
  • 30
  • The basic approach is fine. If it works why would you want to mess with it? Do you really need it to be faster? Assuming that the formula entry comes from a user typing, the answer will surely be instantaneous for all practical purposes. And if you're thinking of any modification that makes the code harder to read and understand, just don't go there. – Paul Cornelius May 09 '16 at 23:46
  • Hi. This function will be called thousands of times from other functions. It also looks fine to me. But since im new in python there could be something that i was doing that took a big toll in performance. – Jorge Canelhas May 10 '16 at 08:28
  • Makes sense, thanks for the additional info. I would still try it and see if it's fast enough before doing anything else. At a guess, I would expect the conversion of string to float `(float(val))` to take more time than anything else here. – Paul Cornelius May 11 '16 at 21:07
  • The formula entry comes from a genetic algorithm that generates prgrams to approach a given function, this is part of a propject to tackle regression problems. Its mainly academic, thus the needed readability. – Jorge Canelhas Aug 05 '16 at 14:18

2 Answers2

2

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.


def safe_divide(darg1, darg2):
    try:
        return darg1/darg2
    except ZeroDivisionError:
        return ERROR_VALUE

def parseRPN(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 /"
    if isinstance(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 = None
        if 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()


def test_happy_paths():
    assert parseRPN([2, 3, '*']) == 6.
    assert parseRPN('0 sin') == 0.
    assert parseRPN([2, 3, '/']) == 2./3

def test_safe_divide():
    assert parseRPN([2, 0, '/']) == ERROR_VALUE

def test_parse_error():
    with pytest.raises(ValueError) as excinfo:
        parseRPN('gin')
    assert str(excinfo.value) == 'could not convert string to float: gin'
johntellsall
  • 14,394
  • 4
  • 46
  • 40
1

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


def postfix(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.
    """
    if isinstance(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.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94