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