Safe way to parse user-supplied mathematical formula in Python

后端 未结 3 1896
忘了有多久
忘了有多久 2020-11-30 03:11

Is there a math expressions parser + evaluator for Python?

I am not the first to ask this question, but answers usually point to eval(). For instance,

相关标签:
3条回答
  • 2020-11-30 03:58

    I'd suggest using ast.parse and then whitelisting the parse tree.

    tree = ast.parse(s, mode='eval')
    valid = all(isinstance(node, whitelist) for node in ast.walk(tree))
    if valid:
        result = eval(compile(tree, filename='', mode='eval'),
                      {"__builtins__": None}, safe_dict)
    

    Here whitelist could be something like:

    whitelist = (ast.Expression, ast.Call, ast.Name, ast.Load,
                 ast.BinOp, ast.UnaryOp, ast.operator, ast.unaryop, ast.cmpop,
                 ast.Num,
                )
    
    0 讨论(0)
  • 2020-11-30 04:04

    I built upon a few posts here to make an evaluator class. Also used eval example which I basically rewrote into a class object.

    import sys
    import ast
    import operator as op
    import abc
    
    import math
    
    class IEvaluator:
        __metaclass__ = abc.ABCMeta
    
        @abc.abstractmethod
        def eval_expr(cls, expr, subs):  # @NoSelf
            '''IMPORTANT: this is class method, overload it with @classmethod!
            Evaluate an expression given in the expr string.
    
            :param expr: str. String expression.
            :param subs: dict. Dictionary with values to substitute.
            :returns: Evaluated expression result.
            '''
    
    
    class Evaluator(IEvaluator):
        '''Generic evaluator for a string expression. Uses ast and operator
        modules. The expr string is parsed with ast resulting in a node tree.
        Then the node tree is recursively traversed and evaluated with operations
        from the operator module.
    
        :implements: IEvaluator
        '''
    
        @classmethod
        def _get_op(cls, node):
            '''Get the operator corresponding to the node.
            :param node: Operator node type with node.op property.
            '''
            # supported operators
            operators = {
                ast.Add: op.add,
                ast.Sub: op.sub,
                ast.Mult: op.mul,
                ast.Div: op.truediv,
                ast.Pow: op.pow,
                ast.BitXor: op.xor,
                ast.USub: op.neg
            }
            return operators[type(node.op)]
    
        @classmethod
        def _get_op_fun(cls, node):
            # fun_call = {'sin': math.sin, 'cos': math.cos}[node.func.id]
            fun_call = getattr(math, node.func.id)
            return fun_call
    
        @classmethod
        def _num_op(cls, node, subs):
            '''Return the value of the node.
            :param node: Value node type with node.n property.
            '''
            return node.n
    
        @classmethod
        def _bin_op(cls, node, subs):
            '''Eval the left and right nodes, and call the binary operator.
            :param node: Binary operator with node.op, node.left, and node.right
                properties.
            '''
            op = cls._get_op(node)
            left_node = cls.eval(node.left, subs)
            right_node = cls.eval(node.right, subs)
            return op(left_node, right_node)
    
        @classmethod
        def _unary_op(cls, node, subs):
            '''Eval the node operand and call the unary operator.
            :param node: Unary operator with node.op and node.operand properties.
            '''
            op = cls._get_op(node)
            return op(cls.eval(node.operand, subs))
    
        @classmethod
        def _subs_op(cls, node, subs):
            '''Return the value of the variable represented by the node.
            :param node: Name node with node.id property to identify the variable.
            '''
            try:
                return subs[node.id]
            except KeyError:
                raise TypeError(node)
    
        @classmethod
        def _call_op(cls, node, subs):
            arg_list = []
            for node_arg in node.args:
                arg_list.append(cls.eval(node_arg, subs))
            fun_call = cls._get_op_fun(node)
            return fun_call(*arg_list)
    
        @classmethod
        def eval(cls, node, subs):
            '''The node is actually a tree. The node type i.e. type(node) is:
                ast.Num, ast.BinOp, ast.UnaryOp or ast.Name.
            Depending on the node type the node will have the following properties:
                node.n - Nodes value.
                node.id - Node id corresponding to a key in the subs dictionary.
                node.op - operation node. Type of node.op identifies the operation.
                    type(node.op) is one of ast.Add, ast.Sub, ast.Mult, ast.Div,
                    ast.Pow, ast.BitXor, or ast.USub.
                node.left or node.right - Binary operation node needs to have links
                    to left and right nodes.
                node.operand - Unary operation node needs to have an operand.
    
            The binary and unary operations call eval recursively.
            '''
            # The functional logic is:
            # if isinstance(node, ast.Num):  # <number>
            #     return node.n
            # elif isinstance(node, ast.BinOp):  # <left> <operator> <right>
            #     return operators[type(node.op)](eval_(node.left, subs),
            #                                     eval_(node.right, subs))
            # elif isinstance(node, ast.UnaryOp):  # <operator> <operand> e.g., -1
            #     return operators[type(node.op)](eval_(node.operand, subs))
            # else:
            #     try:
            #         return subs[node.id]
            #     except KeyError:
            #         raise TypeError(node)
    
            node_type = type(node)
    
            return {
                # Value in the expression. Leaf.
                ast.Num: cls._num_op,  # <number>
    
                # Bin operation with two operands.
                ast.BinOp: cls._bin_op,  # <left> <operator> <right>
    
                # Unary operation such as neg.
                ast.UnaryOp: cls._unary_op,  # <operator> <operand> e.g., -1
    
                # Sub the value for the variable. Leaf.
                ast.Name: cls._subs_op,  # <variable>
    
                ast.Call: cls._call_op
    
            }[node_type](node, subs)
    
        @classmethod
        def eval_expr(cls, expr, subs=None):
            '''Evaluates a string expression. The expr string is parsed with ast
            resulting in a node tree. Then the eval method is used to recursively
            traverse and evaluate the nodes. Symbolic params are taken from subs.
    
            :Example:
                >>> eval_expr('2^6')
                4
                >>> eval_expr('2**6')
                64
                >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
                -5.0
                >>> eval_expr('x + y', {'x': 1, 'y': 2})
                3
    
            :param expr: str. String expression.
            :param subs: dict. (default: globals of current and calling stack.)
            :returns: Result of running the evaluator.
    
            :implements: IEvaluator.eval_expr
    
            '''
            # ref: https://stackoverflow.com/a/9558001/3457624
            if subs is None:
                # Get the globals
                frame = sys._getframe()
                subs = {}
                subs.update(frame.f_globals)
    
                if frame.f_back:
                    subs.update(frame.f_back.f_globals)
    
            expr_tree = ast.parse(expr, mode='eval').body
            return cls.eval(expr_tree, subs)
    

    Here are some examples:

    import sympy
    
    from eval_sympy import Evaluator
    
    # test case...
    x = sympy.Symbol('x')
    y = sympy.Symbol('y')
    
    expr = x * 2 - y ** 2
    # z = expr.subs({x:1, y:2})
    
    str_expr = str(expr)
    print str_expr
    
    x = 1
    y = 2
    out0 = Evaluator.eval_expr(str_expr)
    print '(x, y): ({}, {})'.format(x, y)
    print str_expr, ' = ', out0
    
    subs1 = {'x': 1, 'y': 2}
    out1 = Evaluator.eval_expr(str_expr, subs1)
    print 'subs: ', subs1
    print str_expr, ' = ', out1
    
    sin_subs = {'x': 1, 'y': 2}
    sin_out = Evaluator.eval_expr('sin(log10(x*y))', sin_subs)
    print 'sin_subs: ', sin_subs
    print 'sin(log10(x*y)) = ', sin_out
    

    Results

    2*x - y**2
    
    (x, y): (1, 2)
    2*x - y**2  =  -2
    
    subs:  {'y': 2, 'x': 1}
    2*x - y**2  =  -2
    
    sin_subs:  {'y': 2, 'x': 1}
    sin(log10(x*y)) =  0.296504042171
    
    0 讨论(0)
  • 2020-11-30 04:07

    Check out Paul McGuire's pyparsing. He has written both the general parser and a grammar for arithmetic expressions:

    from __future__ import division
    import pyparsing as pyp
    import math
    import operator
    
    class NumericStringParser(object):
        '''
        Most of this code comes from the fourFn.py pyparsing example
        http://pyparsing.wikispaces.com/file/view/fourFn.py
        http://pyparsing.wikispaces.com/message/view/home/15549426
        __author__='Paul McGuire'
    
        All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
        more easily in other places.
        '''
        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 = pyp.Literal( "." )
            e     = pyp.CaselessLiteral( "E" )
            fnumber = pyp.Combine( pyp.Word( "+-"+pyp.nums, pyp.nums ) + 
                               pyp.Optional( point + pyp.Optional( pyp.Word( pyp.nums ) ) ) +
                               pyp.Optional( e + pyp.Word( "+-"+pyp.nums, pyp.nums ) ) )
            ident = pyp.Word(pyp.alphas, pyp.alphas+pyp.nums+"_$")       
            plus  = pyp.Literal( "+" )
            minus = pyp.Literal( "-" )
            mult  = pyp.Literal( "*" )
            div   = pyp.Literal( "/" )
            lpar  = pyp.Literal( "(" ).suppress()
            rpar  = pyp.Literal( ")" ).suppress()
            addop  = plus | minus
            multop = mult | div
            expop = pyp.Literal( "^" )
            pi    = pyp.CaselessLiteral( "PI" )
            expr = pyp.Forward()
            atom = ((pyp.Optional(pyp.oneOf("- +")) +
                     (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst))
                    | pyp.Optional(pyp.oneOf("- +")) + pyp.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 = pyp.Forward()
            factor << atom + pyp.ZeroOrMore( ( expop + factor ).setParseAction(
                self.pushFirst ) )
            term = factor + pyp.ZeroOrMore( ( multop + factor ).setParseAction(
                self.pushFirst ) )
            expr << term + pyp.ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) )
            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,
                    "abs" : abs,
                    "trunc" : lambda a: int(a),
                    "round" : round,
                    # For Python3 compatibility, cmp replaced by ((a > 0) - (a < 0)). See
                    # https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
                    "sgn" : lambda a: abs(a)>epsilon and ((a > 0) - (a < 0)) or 0}
            self.exprStack = []
        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
    
    nsp = NumericStringParser()
    print(nsp.eval('1+2'))
    # 3.0
    
    print(nsp.eval('2*3-5'))
    # 1.0
    
    0 讨论(0)
提交回复
热议问题