Может ли этот постфиксный интерпретатор Python (обратная польская нотация) быть более эффективным и точным?

вот интерпретатор постфиксных обозначений Python, который использует стек для оценки выражений. Можно ли сделать эту функцию более эффективной и точной?

#!/usr/bin/env python


import operator
import doctest


class Stack:
    """A stack is a collection, meaning that it is a data structure that 
    contains multiple elements.

    """

    def __init__(self):
        """Initialize a new empty stack."""
        self.items = []       

    def push(self, item):
        """Add a new item to the stack."""
        self.items.append(item)

    def pop(self):
        """Remove and return an item from the stack. The item 
        that is returned is always the last one that was added.

        """
        return self.items.pop()

    def is_empty(self):
        """Check whether the stack is empty."""
        return (self.items == [])


# Map supported arithmetic operators to their functions
ARITHMETIC_OPERATORS = {"+":"add", "-":"sub", "*":"mul", "/":"div", 
                        "%":"mod", "**":"pow", "//":"floordiv"}


def postfix(expression, stack=Stack(), operators=ARITHMETIC_OPERATORS):
    """Postfix is a mathematical notation wherein every operator follows all 
    of its operands. This function accepts a string as a postfix mathematical 
    notation and evaluates the expressions. 

    1. Starting at the beginning of the expression, get one term 
       (operator or operand) at a time.
       * If the term is an operand, push it on the stack.
       * If the term is an operator, pop two operands off the stack, 
         perform the operation on them, and push the result back on 
         the stack.

    2. When you get to the end of the expression, there should be exactly 
       one operand left on the stack. That operand is the result.

    See http://en.wikipedia.org/wiki/Reverse_Polish_notation

    >>> expression = "1 2 +"
    >>> postfix(expression)
    3
    >>> expression = "5 4 3 + *"
    >>> postfix(expression)
    35
    >>> expression = "3 4 5 * -"
    >>> postfix(expression)
    -17
    >>> expression = "5 1 2 + 4 * + 3 -"
    >>> postfix(expression, Stack(), ARITHMETIC_OPERATORS)
    14

    """
    if not isinstance(expression, str):
        return
    for val in expression.split(" "):
        if operators.has_key(val):
            method = getattr(operator, operators.get(val))
            # The user has not input sufficient values in the expression
            if len(stack.items) < 2:
                return
            first_out_one = stack.pop()
            first_out_two = stack.pop()
            operand = method(first_out_two, first_out_one)
            stack.push(operand)
        else:
            # Type check and force int
            try:
                operand = int(val)
                stack.push(operand)
            except ValueError:
                continue
    return stack.pop()


if __name__ == '__main__':
    doctest.testmod()

3 ответов


общие рекомендации:

  • избегайте ненужных проверок типов и полагайтесь на поведение исключений по умолчанию.
  • has_key() уже давно не рекомендуется в пользу in оператор: используйте это вместо этого.
  • профиль ваша программа, прежде чем пытаться оптимизировать производительность. Для запуска профилирования с нулевым усилием любого заданного кода просто запустите:python -m cProfile -s cumulative foo.py

конкретные очки:

  • list делает хороший стек из коробки. В частности, он позволяет использовать диапазонное представление (учебник), чтобы заменить pop/pop/append танец с одним шагом.
  • ARITHMETIC_OPERATORS может ссылаться на реализации операторов напрямую, без getattr косвенности.

положить все это вместе:

ARITHMETIC_OPERATORS = {
    '+':  operator.add, '-':  operator.sub,
    '*':  operator.mul, '/':  operator.div, '%':  operator.mod,
    '**': operator.pow, '//': operator.floordiv,
}

def postfix(expression, operators=ARITHMETIC_OPERATORS):
    stack = []
    for val in expression.split():
        if val in operators:
            f = operators[val]
            stack[-2:] = [f(*stack[-2:])]
        else:
            stack.append(int(val))
    return stack.pop()

списки могут использоваться непосредственно как стеки:

>>> stack = []
>>> stack.append(3) # push
>>> stack.append(2)
>>> stack
[3, 2]
>>> stack.pop() # pop
2
>>> stack
[3]

вы также можете поместить функции оператора непосредственно в свой ARITHMETIC_OPERATORS dict:

ARITHMETIC_OPERATORS = {"+":operator.add,
                        "-":operator.sub,
                        "*":operator.mul,
                        "/":operator.div, 
                        "%":operator.mod,
                        "**":operator.pow,
                        "//":operator.floordiv}

затем

if operators.has_key(val):
    method = operators[val]

кстати, использование списков в качестве стеков довольно общие идиоматический Python.


вы можете напрямую сопоставить операторы: {"+": operator.add, "-": operator.sub, ...}. Это проще, не нужно лишних getattr а также позволяет добавлять дополнительные функции (без взлома модуля оператора). Вы также можете удалить несколько временных переменных, которые используются только один раз:

rhs, lhs = stack.pop(), stack.pop()
stack.push(operators[val](lhs, rhs)).    

кроме того (меньше производительности и больше проблемы стиля, также субъективной), я бы не делал обработку ошибок вообще в цикле и обернуть его в один блок try с except KeyError заблокировать ("неизвестный операнд"), к except IndexError блок (пустой стек), ...

но точный? Дает ли это неправильные результаты?