Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Evaluating a mathematical expression without eval() on Python3

I'm working on a "copy-paste calculator" that detects any mathematical expressions copied to the system clipboard, evaluates them and copies the answer to the clipboard ready to be pasted. However, while the code uses the eval()-function, I'm not terribly concerned considering the user normally knows what they are copying. That being said, I want to find a better way without giving the calculations a handicap (= eg. removing the ability to calculate multiplications or exponents).

Here's the important parts of my code:

#! python3
import pyperclip, time

parsedict = {"×": "*",
             "÷": "/",
             "^": "**"} # Get rid of anything that cannot be evaluated

def stringparse(string): # Remove whitespace and replace unevaluateable objects
    a = string
    a = a.replace(" ", "")
    for i in a:
        if i in parsedict.keys():
            a = a.replace(i, parsedict[i])
    print(a)
    return a

def calculate(string):
    parsed = stringparse(string)
    ans = eval(parsed) # EVIL!!!
    print(ans)
    pyperclip.copy(str(ans))

def validcheck(string): # Check if the copied item is a math expression
    proof = 0
    for i in mathproof:
        if i in string:
            proof += 1
        elif "http" in string: #TODO: Create a better way of passing non-math copies
            proof = 0
            break
    if proof != 0:
        calculate(string)

def init(): # Ensure previous copies have no effect
    current = pyperclip.paste()
    new = current
    main(current, new)

def main(current, new):
    while True:
        new = pyperclip.paste()
        if new != current:
            validcheck(new)
            current = new
            pass
        else:
            time.sleep(1.0)
            pass

if __name__ == "__main__":
    init()

Q: What should I use instead of eval() to calculate the answer?

like image 556
Diapolo10 Avatar asked Aug 09 '16 21:08

Diapolo10


2 Answers

You should use ast.parse:

import ast

try:
    tree = ast.parse(expression, mode='eval')
except SyntaxError:
    return    # not a Python expression
if not all(isinstance(node, (ast.Expression,
        ast.UnaryOp, ast.unaryop,
        ast.BinOp, ast.operator,
        ast.Num)) for node in ast.walk(tree)):
    return    # not a mathematical expression (numbers and operators)
result = eval(compile(tree, filename='', mode='eval'))

Note that for simplicity this allows all the unary operators (+, -, ~, not) as well as the arithmetic and bitwise binary operators (+, -, *, /, %, // **, <<, >>, &, |, ^) but not the logical or comparison operators. If should be straightforward to refine or expand the allowed operators.

like image 56
ecatmur Avatar answered Oct 18 '22 20:10

ecatmur


without using eval, you'd have to implement a parser, or use existing packages like simpleeval (I'm not the author, and there are others, but I have tested that one successfully)

In one line, plus import:

>>> from simpleeval import simpleeval
>>> simpleeval.simple_eval("(45 + -45) + 34")
34
>>> simpleeval.simple_eval("(45 - 22*2) + 34**2")
1157

now if I try to hack the calculator by trying to import a module:

>>> simpleeval.simple_eval("import os")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "K:\CODE\COTS\python\simpleeval\simpleeval.py", line 466, in simple_eval
    return s.eval(expr)
  File "K:\CODE\COTS\python\simpleeval\simpleeval.py", line 274, in eval
    return self._eval(ast.parse(expr.strip()).body[0].value)
AttributeError: 'Import' object has no attribute 'value'

Caught! the cryptic error message comes from the fact that simpleeval can evaluate variables that you can optionally pass through a dictionary. Catch AttributeError exception to intercept wrongly formed expressions. No need for eval for that.

like image 1
Jean-François Fabre Avatar answered Oct 18 '22 21:10

Jean-François Fabre