Evaluating a mathematical expression in a string

前端 未结 11 1195
名媛妹妹
名媛妹妹 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:09

    Use eval in a clean namespace:

    >>> ns = {'__builtins__': None}
    >>> eval('2 ** 4', ns)
    16
    

    The clean namespace should prevent injection. For instance:

    >>> eval('__builtins__.__import__("os").system("echo got through")', ns)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 1, in <module>
    AttributeError: 'NoneType' object has no attribute '__import__'
    

    Otherwise you would get:

    >>> eval('__builtins__.__import__("os").system("echo got through")')
    got through
    0
    

    You might want to give access to the math module:

    >>> import math
    >>> ns = vars(math).copy()
    >>> ns['__builtins__'] = None
    >>> eval('cos(pi/3)', ns)
    0.50000000000000011
    
    0 讨论(0)
  • 2020-11-21 05:13

    eval is evil

    eval("__import__('os').remove('important file')") # arbitrary commands
    eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory
    

    Note: even if you use set __builtins__ to None it still might be possible to break out using introspection:

    eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})
    

    Evaluate arithmetic expression using ast

    import ast
    import operator as op
    
    # 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}
    
    def eval_expr(expr):
        """
        >>> eval_expr('2^6')
        4
        >>> eval_expr('2**6')
        64
        >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
        -5.0
        """
        return eval_(ast.parse(expr, mode='eval').body)
    
    def eval_(node):
        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), eval_(node.right))
        elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
            return operators[type(node.op)](eval_(node.operand))
        else:
            raise TypeError(node)
    

    You can easily limit allowed range for each operation or any intermediate result, e.g., to limit input arguments for a**b:

    def power(a, b):
        if any(abs(n) > 100 for n in [a, b]):
            raise ValueError((a,b))
        return op.pow(a, b)
    operators[ast.Pow] = power
    

    Or to limit magnitude of intermediate results:

    import functools
    
    def limit(max_=None):
        """Return decorator that limits allowed returned values."""
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                ret = func(*args, **kwargs)
                try:
                    mag = abs(ret)
                except TypeError:
                    pass # not applicable
                else:
                    if mag > max_:
                        raise ValueError(ret)
                return ret
            return wrapper
        return decorator
    
    eval_ = limit(max_=10**100)(eval_)
    

    Example

    >>> evil = "__import__('os').remove('important file')"
    >>> eval_expr(evil) #doctest:+IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    TypeError:
    >>> eval_expr("9**9")
    387420489
    >>> eval_expr("9**9**9**9**9**9**9**9") #doctest:+IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    ValueError:
    
    0 讨论(0)
  • 2020-11-21 05:14

    This is a massively late reply, but I think useful for future reference. Rather than write your own math parser (although the pyparsing example above is great) you could use SymPy. I don't have a lot of experience with it, but it contains a much more powerful math engine than anyone is likely to write for a specific application and the basic expression evaluation is very easy:

    >>> import sympy
    >>> x, y, z = sympy.symbols('x y z')
    >>> sympy.sympify("x**3 + sin(y)").evalf(subs={x:1, y:-3})
    0.858879991940133
    

    Very cool indeed! A from sympy import * brings in a lot more function support, such as trig functions, special functions, etc., but I've avoided that here to show what's coming from where.

    0 讨论(0)
  • The reason eval and exec are so dangerous is that the default compile function will generate bytecode for any valid python expression, and the default eval or exec will execute any valid python bytecode. All the answers to date have focused on restricting the bytecode that can be generated (by sanitizing input) or building your own domain-specific-language using the AST.

    Instead, you can easily create a simple eval function that is incapable of doing anything nefarious and can easily have runtime checks on memory or time used. Of course, if it is simple math, than there is a shortcut.

    c = compile(stringExp, 'userinput', 'eval')
    if c.co_code[0]==b'd' and c.co_code[3]==b'S':
        return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]
    

    The way this works is simple, any constant mathematic expression is safely evaluated during compilation and stored as a constant. The code object returned by compile consists of d, which is the bytecode for LOAD_CONST, followed by the number of the constant to load (usually the last one in the list), followed by S, which is the bytecode for RETURN_VALUE. If this shortcut doesn't work, it means that the user input isn't a constant expression (contains a variable or function call or similar).

    This also opens the door to some more sophisticated input formats. For example:

    stringExp = "1 + cos(2)"
    

    This requires actually evaluating the bytecode, which is still quite simple. Python bytecode is a stack oriented language, so everything is a simple matter of TOS=stack.pop(); op(TOS); stack.put(TOS) or similar. The key is to only implement the opcodes that are safe (loading/storing values, math operations, returning values) and not unsafe ones (attribute lookup). If you want the user to be able to call functions (the whole reason not to use the shortcut above), simple make your implementation of CALL_FUNCTION only allow functions in a 'safe' list.

    from dis import opmap
    from Queue import LifoQueue
    from math import sin,cos
    import operator
    
    globs = {'sin':sin, 'cos':cos}
    safe = globs.values()
    
    stack = LifoQueue()
    
    class BINARY(object):
        def __init__(self, operator):
            self.op=operator
        def __call__(self, context):
            stack.put(self.op(stack.get(),stack.get()))
    
    class UNARY(object):
        def __init__(self, operator):
            self.op=operator
        def __call__(self, context):
            stack.put(self.op(stack.get()))
    
    
    def CALL_FUNCTION(context, arg):
        argc = arg[0]+arg[1]*256
        args = [stack.get() for i in range(argc)]
        func = stack.get()
        if func not in safe:
            raise TypeError("Function %r now allowed"%func)
        stack.put(func(*args))
    
    def LOAD_CONST(context, arg):
        cons = arg[0]+arg[1]*256
        stack.put(context['code'].co_consts[cons])
    
    def LOAD_NAME(context, arg):
        name_num = arg[0]+arg[1]*256
        name = context['code'].co_names[name_num]
        if name in context['locals']:
            stack.put(context['locals'][name])
        else:
            stack.put(context['globals'][name])
    
    def RETURN_VALUE(context):
        return stack.get()
    
    opfuncs = {
        opmap['BINARY_ADD']: BINARY(operator.add),
        opmap['UNARY_INVERT']: UNARY(operator.invert),
        opmap['CALL_FUNCTION']: CALL_FUNCTION,
        opmap['LOAD_CONST']: LOAD_CONST,
        opmap['LOAD_NAME']: LOAD_NAME
        opmap['RETURN_VALUE']: RETURN_VALUE,
    }
    
    def VMeval(c):
        context = dict(locals={}, globals=globs, code=c)
        bci = iter(c.co_code)
        for bytecode in bci:
            func = opfuncs[ord(bytecode)]
            if func.func_code.co_argcount==1:
                ret = func(context)
            else:
                args = ord(bci.next()), ord(bci.next())
                ret = func(context, args)
            if ret:
                return ret
    
    def evaluate(expr):
        return VMeval(compile(expr, 'userinput', 'eval'))
    

    Obviously, the real version of this would be a bit longer (there are 119 opcodes, 24 of which are math related). Adding STORE_FAST and a couple others would allow for input like 'x=5;return x+x or similar, trivially easily. It can even be used to execute user-created functions, so long as the user created functions are themselves executed via VMeval (don't make them callable!!! or they could get used as a callback somewhere). Handling loops requires support for the goto bytecodes, which means changing from a for iterator to while and maintaining a pointer to the current instruction, but isn't too hard. For resistance to DOS, the main loop should check how much time has passed since the start of the calculation, and certain operators should deny input over some reasonable limit (BINARY_POWER being the most obvious).

    While this approach is somewhat longer than a simple grammar parser for simple expressions (see above about just grabbing the compiled constant), it extends easily to more complicated input, and doesn't require dealing with grammar (compile take anything arbitrarily complicated and reduces it to a sequence of simple instructions).

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

    You can use the ast module and write a NodeVisitor that verifies that the type of each node is part of a whitelist.

    import ast, math
    
    locals =  {key: value for (key,value) in vars(math).items() if key[0] != '_'}
    locals.update({"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round})
    
    class Visitor(ast.NodeVisitor):
        def visit(self, node):
           if not isinstance(node, self.whitelist):
               raise ValueError(node)
           return super().visit(node)
    
        whitelist = (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp,
                ast.Mult, ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod,
                ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name)
    
    def evaluate(expr, locals = {}):
        if any(elem in expr for elem in '\n#') : raise ValueError(expr)
        try:
            node = ast.parse(expr.strip(), mode='eval')
            Visitor().visit(node)
            return eval(compile(node, "<string>", "eval"), {'__builtins__': None}, locals)
        except Exception: raise ValueError(expr)
    

    Because it works via a whitelist rather than a blacklist, it is safe. The only functions and variables it can access are those you explicitly give it access to. I populated a dict with math-related functions so you can easily provide access to those if you want, but you have to explicitly use it.

    If the string attempts to call functions that haven't been provided, or invoke any methods, an exception will be raised, and it will not be executed.

    Because this uses Python's built in parser and evaluator, it also inherits Python's precedence and promotion rules as well.

    >>> evaluate("7 + 9 * (2 << 2)")
    79
    >>> evaluate("6 // 2 + 0.0")
    3.0
    

    The above code has only been tested on Python 3.

    If desired, you can add a timeout decorator on this function.

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

    [I know this is an old question, but it is worth pointing out new useful solutions as they pop up]

    Since python3.6, this capability is now built into the language, coined "f-strings".

    See: PEP 498 -- Literal String Interpolation

    For example (note the f prefix):

    f'{2**4}'
    => '16'
    
    0 讨论(0)
提交回复
热议问题