Safely evaluate simple string equation

I’m writing a program in which an equation is inputted as a string, then evaluated. So far, I’ve come up with this:

test_24_string = str(input("Enter your answer: "))
test_24 = eval(test_24_string)

I need both a string version of this equation and an evaluated version. However, eval is a very dangerous function. Using int() doesn’t work, though, because it’s an equation. Is there a Python function that will evaluate a mathematical expression from a string, as if inputting a number?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

One way would be to use . It’s mostly a module for optimizing (and multithreading) operations but it can also handle mathematical python expressions:

>>> import numexpr
>>> numexpr.evaluate('2 + 4.1 * 3')
array(14.299999999999999)

You can call .item on the result to get a python-like type:

>>> numexpr.evaluate('17 / 3').item()
5.666666666666667

It’s a 3rd party extension module so it may be total overkill here but it’s definetly safer than eval and supports quite a number of functions (including numpy and math operations). If also supports “variable substitution”:

>>> b = 10
>>> numexpr.evaluate('exp(17) / b').item()
2415495.27535753

One way with the python standard library, although very limited is ast.literal_eval. It works for the most basic data types and literals in Python:

>>> import ast
>>> ast.literal_eval('1+2')
3

But fails with more complicated expressions like:

>>> ast.literal_eval('import os')
SyntaxError: invalid syntax

>>> ast.literal_eval('exec(1+2)')
ValueError: malformed node or string: <_ast.Call object at 0x0000023BDEADB400>

Unfortunatly any operator besides + and - isn’t possible:

>>> ast.literal_eval('1.2 * 2.3')
ValueError: malformed node or string: <_ast.BinOp object at 0x0000023BDEF24B70>

I copied part of the documentation here that contains the supported types:

Safely evaluate an expression node or a string containing a Python literal or container display. The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None.

Method 2

It is not that difficult to write a postfix expression evaluator. Below is a working example. (Also available on github.)

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]

Usage:

In [2]: from postfix import postfix

In [3]: postfix('1 2 + 7 /')
Out[3]: 0.42857142857142855

In [4]: 3/7
Out[4]: 0.42857142857142855

Method 3

I did this for me needs to answer the same question. It is easy to adapt.

import math
import ast
import operator as op

class MathParser:
    """ Basic parser with local variable and math functions 
    
    Args:
       vars (mapping): mapping object where obj[name] -> numerical value 
       math (bool, optional): if True (default) all math function are added in the same name space
       
    Example:
       
       data = {'r': 3.4, 'theta': 3.141592653589793}
       parser = MathParser(data)
       assert parser.parse('r*cos(theta)') == -3.4
       data['theta'] =0.0
       assert parser.parse('r*cos(theta)') == 3.4
    """
        
    _operators2method = {
        ast.Add: op.add, 
        ast.Sub: op.sub, 
        ast.BitXor: op.xor, 
        ast.Or:  op.or_, 
        ast.And: op.and_, 
        ast.Mod:  op.mod,
        ast.Mult: op.mul,
        ast.Div:  op.truediv,
        ast.Pow:  op.pow,
        ast.FloorDiv: op.floordiv,              
        ast.USub: op.neg, 
        ast.UAdd: lambda a:a  
    }
    
    def __init__(self, vars, math=True):
        self._vars = vars
        if not math:
            self._alt_name = self._no_alt_name
        
    def _Name(self, name):
        try:
            return  self._vars[name]
        except KeyError:
            return self._alt_name(name)
                
    @staticmethod
    def _alt_name(name):
        if name.startswith("_"):
            raise NameError(f"{name!r}") 
        try:
            return  getattr(math, name)
        except AttributeError:
            raise NameError(f"{name!r}") 
    
    @staticmethod
    def _no_alt_name(name):
        raise NameError(f"{name!r}") 
    
    def eval_(self, node):
        if isinstance(node, ast.Expression):
            return self.eval_(node.body)
        if isinstance(node, ast.Num): # <number>
            return node.n
        if isinstance(node, ast.Name):
            return self._Name(node.id) 
        if isinstance(node, ast.BinOp):            
            method = self._operators2method[type(node.op)]                      
            return method( self.eval_(node.left), self.eval_(node.right) )            
        if isinstance(node, ast.UnaryOp):             
            method = self._operators2method[type(node.op)]  
            return method( self.eval_(node.operand) )
        if isinstance(node, ast.Attribute):
            return getattr(self.eval_(node.value), node.attr)
            
        if isinstance(node, ast.Call):            
            return self.eval_(node.func)( 
                      *(self.eval_(a) for a in node.args),
                      **{k.arg:self.eval_(k.value) for k in node.keywords}
                     )           
            return self.Call( self.eval_(node.func), tuple(self.eval_(a) for a in node.args))
        else:
            raise TypeError(node)
    
    def parse(self, expr):
        return  self.eval_(ast.parse(expr, mode='eval'))          
    

Test & Usage

    assert MathParser({"x":4.5}).parse('x*2') == 9
    assert MathParser({}).parse('cos(pi)') == -1.0
        
    data = {'r': 3.4, 'theta': 3.141592653589793}
    parser = MathParser(data)
    assert parser.parse('r*cos(theta)') == -3.4
    data['theta'] = 0.0
    assert parser.parse('r*cos(theta)') == 3.4
    assert MathParser(globals()).parse('math.pi') == math.pi
    
    assert MathParser({'f':lambda x,n=10: x*n}).parse('f(2,20)') == 40
    assert MathParser({'f':lambda x,n=10: x*n}).parse('f(2,n=20)') == 40    

Method 4

I had the same problem and settled with this:

def safe_math_eval(string):
    allowed_chars = "0123456789+-*(). /"
    for char in string:
        if char not in allowed_chars:
            raise Exception("Unsafe eval")

    return eval(string)

There could still be a security issue in there which I can’t see.
If there is an security issue please tell me.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x