RPN Evaluator optimization without losing readability

一曲冷凌霜 提交于 2019-12-14 03:16:40

问题


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


回答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.


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'



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


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.



来源:https://stackoverflow.com/questions/37126866/rpn-evaluator-optimization-without-losing-readability

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!