HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Small Python calculator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
calculatorsmallpython

Problem

I created this small Python calculator after abstaining from writing code for a while now. I would love seasoned programmers input.

```
#!/usr/bin/env python

" A Simple calculator "

class Calculator(object):
""" Example input:
expression: (12+4)2^4-(10-3(15/5))
steps:
1) compile it to a list of ops and numbers: [ [12,+,4],,2,^,4,-,[10,-,3,,[15,/,5]] ]
2) calculate starting with the highest operators first:

[ [12, +, 5], , 16, -, [10,-,3,,[15,/,5]] ]
[ 17, , 16, -, [10,-,3,,3] ]
[ 17, *, 16, -, [10-9] ]
[ 17, *, 16, -, 1]
[ 272, -, 1 ]
[ 271 ]

TODO:
* add floating point support

"""
_stack = []

" Flag that signfies if it's the first character in the expression "
INITIAL = True

" exit perenthesis "
EXIT_PE = False
" in number "
IN_NU = False
" in operator "
IN_OP = False

OPERATORS = ('+', '-', '*', '/', '^')
OP_ORDER = (('^',), ('*', '/'), ('+', '-'))

def compile(self, input_eq):
for c in input_eq:
try:
" check if its a number "
current = int(c)
self.IN_OP = False
" if it's a new digit to a previous number "
if self.IN_NU:
" add it to the previous number "
self._add_new_num(current)
else:
" it's a new number add it to stack "
self._add_new_num(current)
self.IN_NU = True
except ValueError:
self.IN_NU = False
" if it's an operator "
if c in self.OPERATORS:
if not self._stack:
raise Exception("You can't start an expression with an operator")
if self.IN_OP:
raise Exception("illegal expression")
else:
sel

Solution

Use actual comments for comments. All of those strings are kept by the interpreter. If you have to use a string as a documentation item then you should use the triple quote variety.

Use full words or at least standard abbreviations. 'EXIT_PE' <-- no one else knows what this is. The proper spelling is 'parenthesis' singular or 'parentheses' plural. 'EXITS_PARENS' would be a good name. Same goes for 'IN_NU'. 'IN_NUM' would be acceptable.

Limit the scope of your try/except blocks. In compile you are catching ValueError but is it from the call to int() or the internal methods?

Make your own exceptions. Use these instead of just throwing Exception. It helps your reader (or debugger) know where to look.

You repeat yourself in _get_last_position.

In general, simplify your code and don't be afraid to use a few extra characters.

Here is a more pythonic approach. I am not 100% happy with it because I repeat myself a little. There is always room for improvement :-)

```
import operator
import string

class EvaluationError(Exception):
pass

class InvalidNumber(Exception):
pass

class InvalidOperator(Exception):
pass

class UnbalancedParens(Exception):
pass

def cast(value):
"""Attempt to turn a value into a number."""
if isinstance(value, (int, float)):
return value

try:
return int(value)
except ValueError:
pass
try:
return float(value)
except ValueError:
pass

raise InvalidNumber(value)

class Operator(object):
def __init__(self, op, precedence):
self._op = op
self._prec = precedence

def __call__(self, *args):
return self._op(*args)

def __lt__(self, op):
return self._prec op._prec

def __eq__(self, op):
return self._prec == op._prec

def __repr__(self):
return repr(self._op)

def __str__(self):
return str(self._op)

class Calculator(object):
operators = {
'+' : Operator(operator.add, 1),
'-' : Operator(operator.sub, 1),
'*' : Operator(operator.mul, 2),
'/' : Operator(operator.div, 2),
'^' : Operator(operator.pow, 3),
}

def __init__(self):
pass

def calculate(self, expr):
"""Parse and evaluate the expression."""
tokens = self.parse(expr)
result = self.evaluate(tokens)
return result

def evaluate(self, tokens, trace=False):
"""Walk the list of tokens and evaluate the result."""
stack = []
for item in tokens:
if isinstance(item, Operator):
if trace:
print stack

b, a = cast(stack.pop()), cast(stack.pop())
result = item(a, b)
stack.append(result)

if trace:
print stack
else: # anything else just goes on the stack
if item.endswith('.'):
raise InvalidNumber(item)
stack.append(item)

if len(stack) > 1:
raise EvaluationError(str(stack))

return stack[0]

def parse(self, expr, trace=False):
"""Take an infix arithmetic expression and return the expression parsed into postfix notation.
Note the numbers are left as strings to be evaluated later.
"""
tokens = []
op_stack = []

last = None

for c in expr:
if c in string.whitespace:
last = c
elif c in string.digits:
value = str(c)
if last and last in string.digits: # number continues, just append it
value = tokens.pop() + value

last = c
tokens.append(value)
elif c == '.':
if last and last in string.digits: # looks like a decimal
tokens.append(tokens.pop() + ".")
else:
raise InvalidParse("misplaced decimal")
elif c == '(':
op_stack.append('(')
elif c == ')':
if not op_stack:
raise UnbalancedParens(c)

# closing parens found, unwind back to the matching open
while op_stack:
curr = op_stack.pop()
if curr is '(':
break
else:
tokens.append(curr)
else: # not a number or a parens, must be an operator
op = self.operators.get(c, None)
if op is None:
raise InvalidOperator(c)

while op_stack:
curr = op_stack[-1]
# the 'is' check prevents comparing an Operator to a string
if curr is '(': # don't leave the current scope
break
elif curr < op:
break
tokens.append(op_stac

Code Snippets

import operator
import string

class EvaluationError(Exception):
    pass


class InvalidNumber(Exception):
    pass


class InvalidOperator(Exception):
    pass


class UnbalancedParens(Exception):
    pass


def cast(value):
    """Attempt to turn a value into a number."""
    if isinstance(value, (int, float)):
        return value

    try:
        return int(value)
    except ValueError:
        pass
    try:
        return float(value)
    except ValueError:
        pass

    raise InvalidNumber(value)


class Operator(object):
    def __init__(self, op, precedence):
        self._op = op
        self._prec = precedence

    def __call__(self, *args):
        return self._op(*args)

    def __lt__(self, op):
        return self._prec < op._prec

    def __gt__(self, op):
        return self._prec > op._prec

    def __eq__(self, op):
        return self._prec == op._prec

    def __repr__(self):
        return repr(self._op)

    def __str__(self):
        return str(self._op)


class Calculator(object):
    operators = {
        '+' : Operator(operator.add, 1),
        '-' : Operator(operator.sub, 1),
        '*' : Operator(operator.mul, 2),
        '/' : Operator(operator.div, 2),
        '^' : Operator(operator.pow, 3),
    }

    def __init__(self):
        pass

    def calculate(self, expr):
        """Parse and evaluate the expression."""
        tokens = self.parse(expr)
        result = self.evaluate(tokens)
        return result

    def evaluate(self, tokens, trace=False):
        """Walk the list of tokens and evaluate the result."""
        stack = []
        for item in tokens:
            if isinstance(item, Operator):
                if trace:
                    print stack

                b, a = cast(stack.pop()), cast(stack.pop())
                result = item(a, b)
                stack.append(result)

                if trace:
                    print stack
            else:  # anything else just goes on the stack
                if item.endswith('.'):
                    raise InvalidNumber(item)
                stack.append(item)

        if len(stack) > 1:
            raise EvaluationError(str(stack))

        return stack[0]

    def parse(self, expr, trace=False):
        """Take an infix arithmetic expression and return the expression parsed into postfix notation.
        Note the numbers are left as strings to be evaluated later.
        """
        tokens = []
        op_stack = []

        last = None

        for c in expr:
            if c in string.whitespace:
                last = c
            elif c in string.digits:
                value = str(c)
                if last and last in string.digits:  # number continues, just append it
                    value = tokens.pop() + value

                last = c
                tokens.append(value)
            elif c == '.':
                if last and last in string.digits:  # looks like a decimal
                    tokens.append(tokens.pop() + ".")
      

Context

StackExchange Code Review Q#46698, answer score: 5

Revisions (0)

No revisions yet.