Safely evaluate simple string equation
Asked Answered
F

5

12

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?

Falsework answered 7/5, 2017 at 21:23 Comment(0)
K
16

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.

Kelleekelleher answered 7/5, 2017 at 21:28 Comment(7)
Weird enough, but literal_eval supports only + and - and only on integers. Other operators generate the same malformed... error. Tested on py3.5.Melmela
@Melmela I can't reproduce the failure with floats (ast.literal_eval('1.2 + 2.3') works correctly). But you're right not all operators are possible. I've added another way - which implements (much) more features.Kelleekelleher
@Kelleekelleher installed numpy. After importing numpy and numexpr, it says ModuleNotFoundError: No module named 'numexpr'. I tried reinstalling.Falsework
@user138292 You also have to install numexpr. It's a different package and not included in numpy.Kelleekelleher
The fact that literal_eval supports + and - was originally a weird side effect of too-permissive support for complex literals (1+2j, 3-4j). I think they decided to call it a feature. There are a bunch of arguments about literal_eval on the bug tracker, and probably more on the mailing lists.Prunelle
ast.literal_eval('1+2') valueerrors for me in Python 3.8.Ration
No. For future visitors: the solution proposed here, numexpr.evaluate, is dangerous and thus wrong. Given that safely evaluating an arithmetic expression is the topic at hand, that the original question explicitly mentions the hazards of eval, and that numexpr.evaluate is using eval under the hood, this answer should not have been accepted. See the following Github issue for more info, whose final comment appears to have been ignored or overlooked: github.com/pydata/numexpr/issues/323#. I have verified the segfault produced in that thread but haven't dug further than that.Boschbok
C
6

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
Corsage answered 7/5, 2017 at 22:36 Comment(0)
O
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.

Otero answered 21/10, 2020 at 20:32 Comment(2)
According to this page, you want eval(string, {"__builtins__":None}, {}) for defence in depth.Jade
...though, as this page explains, that's still unsafe without your allowed_chars filter because of methods like .__subclasses__().Jade
W
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    
Wiegand answered 12/10, 2021 at 12:56 Comment(0)
B
1

The accepted answer is incorrect. Under the hood, numexpr.evaluate relies on eval. See https://github.com/pydata/numexpr/issues/323 for info on how using this library on user input can go wrong.

Instead, here is an eval-free evaluator for arithmetic expressions written by one Paul McGuire: https://github.com/pyparsing/pyparsing/blob/master/examples/fourFn.py. The hard work has already been done. If you added the following snippet to the example code in its current form as of this writing, you would have a safe_eval function capable of arithmetic:

def safe_eval(expression: str) -> float:
    BNF().parseString(expression, parseAll=True)
    return evaluate_stack(exprStack[:])

Note that Paul's example code is intended to demonstrate how to use the parser rather than to provide an arithmetic API, seemingly, so you might want to spruce up the code a bit to match your conventions. See also: Safe way to parse user-supplied mathematical formula in Python

Boschbok answered 22/4, 2023 at 13:3 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.