Evaluating a mathematical expression in a string

前端 未结 11 1185
名媛妹妹
名媛妹妹 2020-11-21 05:01
stringExp = \"2^4\"
intVal = int(stringExp)      # Expected value: 16

This returns the following error:

Traceback (most recent call         


        
相关标签:
11条回答
  • 2020-11-21 05:25

    Okay, so the problem with eval is that it can escape its sandbox too easily, even if you get rid of __builtins__. All the methods for escaping the sandbox come down to using getattr or object.__getattribute__ (via the . operator) to obtain a reference to some dangerous object via some allowed object (''.__class__.__bases__[0].__subclasses__ or similar). getattr is eliminated by setting __builtins__ to None. object.__getattribute__ is the difficult one, since it cannot simply be removed, both because object is immutable and because removing it would break everything. However, __getattribute__ is only accessible via the . operator, so purging that from your input is sufficient to ensure eval cannot escape its sandbox.
    In processing formulas, the only valid use of a decimal is when it is preceded or followed by [0-9], so we just remove all other instances of ..

    import re
    inp = re.sub(r"\.(?![0-9])","", inp)
    val = eval(inp, {'__builtins__':None})
    

    Note that while python normally treats 1 + 1. as 1 + 1.0, this will remove the trailing . and leave you with 1 + 1. You could add ),, and EOF to the list of things allowed to follow ., but why bother?

    0 讨论(0)
  • 2020-11-21 05:25

    Here's my solution to the problem without using eval. Works with Python2 and Python3. It doesn't work with negative numbers.

    $ python -m pytest test.py
    

    test.py

    from solution import Solutions
    
    class SolutionsTestCase(unittest.TestCase):
        def setUp(self):
            self.solutions = Solutions()
    
        def test_evaluate(self):
            expressions = [
                '2+3=5',
                '6+4/2*2=10',
                '3+2.45/8=3.30625',
                '3**3*3/3+3=30',
                '2^4=6'
            ]
            results = [x.split('=')[1] for x in expressions]
            for e in range(len(expressions)):
                if '.' in results[e]:
                    results[e] = float(results[e])
                else:
                    results[e] = int(results[e])
                self.assertEqual(
                    results[e],
                    self.solutions.evaluate(expressions[e])
                )
    

    solution.py

    class Solutions(object):
        def evaluate(self, exp):
            def format(res):
                if '.' in res:
                    try:
                        res = float(res)
                    except ValueError:
                        pass
                else:
                    try:
                        res = int(res)
                    except ValueError:
                        pass
                return res
            def splitter(item, op):
                mul = item.split(op)
                if len(mul) == 2:
                    for x in ['^', '*', '/', '+', '-']:
                        if x in mul[0]:
                            mul = [mul[0].split(x)[1], mul[1]]
                        if x in mul[1]:
                            mul = [mul[0], mul[1].split(x)[0]]
                elif len(mul) > 2:
                    pass
                else:
                    pass
                for x in range(len(mul)):
                    mul[x] = format(mul[x])
                return mul
            exp = exp.replace(' ', '')
            if '=' in exp:
                res = exp.split('=')[1]
                res = format(res)
                exp = exp.replace('=%s' % res, '')
            while '^' in exp:
                if '^' in exp:
                    itm = splitter(exp, '^')
                    res = itm[0] ^ itm[1]
                    exp = exp.replace('%s^%s' % (str(itm[0]), str(itm[1])), str(res))
            while '**' in exp:
                if '**' in exp:
                    itm = splitter(exp, '**')
                    res = itm[0] ** itm[1]
                    exp = exp.replace('%s**%s' % (str(itm[0]), str(itm[1])), str(res))
            while '/' in exp:
                if '/' in exp:
                    itm = splitter(exp, '/')
                    res = itm[0] / itm[1]
                    exp = exp.replace('%s/%s' % (str(itm[0]), str(itm[1])), str(res))
            while '*' in exp:
                if '*' in exp:
                    itm = splitter(exp, '*')
                    res = itm[0] * itm[1]
                    exp = exp.replace('%s*%s' % (str(itm[0]), str(itm[1])), str(res))
            while '+' in exp:
                if '+' in exp:
                    itm = splitter(exp, '+')
                    res = itm[0] + itm[1]
                    exp = exp.replace('%s+%s' % (str(itm[0]), str(itm[1])), str(res))
            while '-' in exp:
                if '-' in exp:
                    itm = splitter(exp, '-')
                    res = itm[0] - itm[1]
                    exp = exp.replace('%s-%s' % (str(itm[0]), str(itm[1])), str(res))
    
            return format(exp)
    
    0 讨论(0)
  • 2020-11-21 05:27

    I think I would use eval(), but would first check to make sure the string is a valid mathematical expression, as opposed to something malicious. You could use a regex for the validation.

    eval() also takes additional arguments which you can use to restrict the namespace it operates in for greater security.

    0 讨论(0)
  • 2020-11-21 05:30

    Some safer alternatives to eval() and sympy.sympify().evalf()*:

    • asteval
    • numexpr

    *SymPy sympify is also unsafe according to the following warning from the documentation.

    Warning: Note that this function uses eval, and thus shouldn’t be used on unsanitized input.

    0 讨论(0)
  • 2020-11-21 05:34

    Pyparsing can be used to parse mathematical expressions. In particular, fourFn.py shows how to parse basic arithmetic expressions. Below, I've rewrapped fourFn into a numeric parser class for easier reuse.

    from __future__ import division
    from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional,
                           ZeroOrMore, Forward, nums, alphas, oneOf)
    import math
    import operator
    
    __author__ = 'Paul McGuire'
    __version__ = '$Revision: 0.0 $'
    __date__ = '$Date: 2009-03-20 $'
    __source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py
    http://pyparsing.wikispaces.com/message/view/home/15549426
    '''
    __note__ = '''
    All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
    more easily in other places.
    '''
    
    
    class NumericStringParser(object):
        '''
        Most of this code comes from the fourFn.py pyparsing example
    
        '''
    
        def pushFirst(self, strg, loc, toks):
            self.exprStack.append(toks[0])
    
        def pushUMinus(self, strg, loc, toks):
            if toks and toks[0] == '-':
                self.exprStack.append('unary -')
    
        def __init__(self):
            """
            expop   :: '^'
            multop  :: '*' | '/'
            addop   :: '+' | '-'
            integer :: ['+' | '-'] '0'..'9'+
            atom    :: PI | E | real | fn '(' expr ')' | '(' expr ')'
            factor  :: atom [ expop factor ]*
            term    :: factor [ multop factor ]*
            expr    :: term [ addop term ]*
            """
            point = Literal(".")
            e = CaselessLiteral("E")
            fnumber = Combine(Word("+-" + nums, nums) +
                              Optional(point + Optional(Word(nums))) +
                              Optional(e + Word("+-" + nums, nums)))
            ident = Word(alphas, alphas + nums + "_$")
            plus = Literal("+")
            minus = Literal("-")
            mult = Literal("*")
            div = Literal("/")
            lpar = Literal("(").suppress()
            rpar = Literal(")").suppress()
            addop = plus | minus
            multop = mult | div
            expop = Literal("^")
            pi = CaselessLiteral("PI")
            expr = Forward()
            atom = ((Optional(oneOf("- +")) +
                     (ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst))
                    | Optional(oneOf("- +")) + Group(lpar + expr + rpar)
                    ).setParseAction(self.pushUMinus)
            # by defining exponentiation as "atom [ ^ factor ]..." instead of
            # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
            # that is, 2^3^2 = 2^(3^2), not (2^3)^2.
            factor = Forward()
            factor << atom + \
                ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
            term = factor + \
                ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
            expr << term + \
                ZeroOrMore((addop + term).setParseAction(self.pushFirst))
            # addop_term = ( addop + term ).setParseAction( self.pushFirst )
            # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
            # expr <<  general_term
            self.bnf = expr
            # map operator symbols to corresponding arithmetic operations
            epsilon = 1e-12
            self.opn = {"+": operator.add,
                        "-": operator.sub,
                        "*": operator.mul,
                        "/": operator.truediv,
                        "^": operator.pow}
            self.fn = {"sin": math.sin,
                       "cos": math.cos,
                       "tan": math.tan,
                       "exp": math.exp,
                       "abs": abs,
                       "trunc": lambda a: int(a),
                       "round": round,
                       "sgn": lambda a: abs(a) > epsilon and cmp(a, 0) or 0}
    
        def evaluateStack(self, s):
            op = s.pop()
            if op == 'unary -':
                return -self.evaluateStack(s)
            if op in "+-*/^":
                op2 = self.evaluateStack(s)
                op1 = self.evaluateStack(s)
                return self.opn[op](op1, op2)
            elif op == "PI":
                return math.pi  # 3.1415926535
            elif op == "E":
                return math.e  # 2.718281828
            elif op in self.fn:
                return self.fn[op](self.evaluateStack(s))
            elif op[0].isalpha():
                return 0
            else:
                return float(op)
    
        def eval(self, num_string, parseAll=True):
            self.exprStack = []
            results = self.bnf.parseString(num_string, parseAll)
            val = self.evaluateStack(self.exprStack[:])
            return val
    

    You can use it like this

    nsp = NumericStringParser()
    result = nsp.eval('2^4')
    print(result)
    # 16.0
    
    result = nsp.eval('exp(2^4)')
    print(result)
    # 8886110.520507872
    
    0 讨论(0)
提交回复
热议问题