Evaluando una expresión matemática en una cadena

stringExp = "2^4" intVal = int(stringExp) # Expected value: 16 

Esto devuelve el siguiente error:

 Traceback (most recent call last): File "", line 1, in  ValueError: invalid literal for int() with base 10: '2^4' 

Sé que eval puede eval esto, pero ¿no existe un método mejor y, lo que es más importante, más seguro para evaluar una expresión matemática que se almacena en una cadena?

Pyparsing se puede utilizar para analizar expresiones matemáticas. En particular, fourFn.py muestra cómo analizar expresiones aritméticas básicas. A continuación, he vuelto a envolver fourFn en una clase de analizador numérico para una reutilización más fácil.

 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 

Puedes usarlo así

 nsp = NumericStringParser() result = nsp.eval('2^4') print(result) # 16.0 result = nsp.eval('exp(2^4)') print(result) # 8886110.520507872 

eval es malvado

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

Nota: incluso si usa set __builtins__ en None , todavía podría ser posible usar introspección:

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

Evaluar la expresión aritmética usando 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): #  return node.n elif isinstance(node, ast.BinOp): #    return operators[type(node.op)](eval_(node.left), eval_(node.right)) elif isinstance(node, ast.UnaryOp): #   eg, -1 return operators[type(node.op)](eval_(node.operand)) else: raise TypeError(node) 

Puede limitar fácilmente el rango permitido para cada operación o cualquier resultado intermedio, por ejemplo, para limitar los argumentos de entrada para 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 

O para limitar la magnitud de los resultados intermedios:

 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_) 

Ejemplo

 >>> 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: 

Algunas alternativas más seguras a eval() y sympy.sympify().evalf() * :

  • asteval
  • numexpr

* SymPy sympify también es inseguro según la siguiente advertencia de la documentación.

Advertencia: tenga en cuenta que esta función utiliza eval y, por lo tanto, no debe utilizarse en entradas no saneadas.

Bien, entonces el problema con eval es que puede escapar de su caja de arena demasiado fácilmente, incluso si te deshaces de __builtins__ . Todos los métodos para escapar de la zona de getattr reducen al uso de getattr u object.__getattribute__ (a través del operador . ) Para obtener una referencia a algún objeto peligroso a través de algún objeto permitido ( ''.__class__.__bases__[0].__subclasses__ o similar). getattr se elimina estableciendo __builtins__ en None . object.__getattribute__ es el más difícil, ya que no se puede eliminar simplemente, porque el object es inmutable y porque eliminarlo lo rompería todo. Sin embargo, __getattribute__ solo es accesible a través de . operador, por lo tanto, eliminar de su entrada es suficiente para garantizar que eval no pueda escapar de su caja de arena.
En el procesamiento de fórmulas, el único uso válido de un decimal es cuando está precedido o seguido por [0-9] , por lo que simplemente eliminamos todas las demás instancias de . .

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

Tenga en cuenta que mientras python normalmente trata 1 + 1. como 1 + 1.0 , esto eliminará el final . y te dejo con 1 + 1 . Usted podría agregar ) , , y EOF a la lista de cosas permitidas a seguir . , pero ¿por qué molestarse?

La razón por la que eval y exec son tan peligrosos es que la función de compile predeterminada generará un código de bytes para cualquier expresión de python válida, y la eval o exec predeterminada ejecutará cualquier bytecode de python válido. Todas las respuestas hasta la fecha se han centrado en restringir el código de byte que se puede generar (mediante la eliminación de datos de entrada) o en la creación de su propio lenguaje específico de dominio utilizando el AST.

En su lugar, puede crear fácilmente una función eval simple que sea incapaz de hacer nada infeliz y puede tener fácilmente controles de tiempo de ejecución en la memoria o el tiempo utilizado. Por supuesto, si es matemática simple, entonces hay un atajo.

 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] 

La forma en que funciona es simple, cualquier expresión matemática constante se evalúa de manera segura durante la comstackción y se almacena como una constante. El objeto de código devuelto por compile consiste en d , que es el bytecode para LOAD_CONST , seguido del número de la constante a cargar (generalmente el último en la lista), seguido de S , que es el bytecode para RETURN_VALUE . Si este acceso directo no funciona, significa que la entrada del usuario no es una expresión constante (contiene una variable o una función llamada o similar).

Esto también abre la puerta a algunos formatos de entrada más sofisticados. Por ejemplo:

 stringExp = "1 + cos(2)" 

Esto requiere realmente evaluar el código de bytes, que todavía es bastante simple. El TOS=stack.pop(); op(TOS); stack.put(TOS) Python es un lenguaje orientado a la stack, así que todo es un asunto simple de TOS=stack.pop(); op(TOS); stack.put(TOS) TOS=stack.pop(); op(TOS); stack.put(TOS) TOS=stack.pop(); op(TOS); stack.put(TOS) o similar. La clave es implementar solo los códigos de operación que sean seguros (cargar / almacenar valores, operaciones matemáticas, valores de retorno) y no inseguros (búsqueda de atributos). Si desea que el usuario pueda llamar a funciones (todo el motivo para no usar el acceso directo anterior), simplemente haga que su implementación de CALL_FUNCTION solo permita funciones en una lista “segura”.

 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')) 

Obviamente, la versión real de esto sería un poco más larga (hay 119 códigos de operación, 24 de los cuales están relacionados con las matemáticas). Agregar STORE_FAST y un par de otros permitiría la entrada como 'x=5;return x+x o similar, trivialmente fácilmente. Incluso se puede usar para ejecutar funciones creadas por el usuario, siempre y cuando las funciones creadas por el usuario se ejecuten a través de VMeval (¡no las conviertas en una aplicación de llamadas o en cualquier otro lugar)! El manejo de los bucles requiere soporte para los goto byte goto , lo que significa cambiar de iterador a while y mantener un puntero a la instrucción actual, pero no es demasiado difícil. Para la resistencia a DOS, el bucle principal debe verificar cuánto tiempo ha pasado desde el inicio del cálculo, y ciertos operadores deben negar la entrada por encima de algún límite razonable ( BINARY_POWER es el más obvio).

Si bien este enfoque es algo más largo que un simple analizador gtwigtical para expresiones simples (ver más arriba acerca de cómo capturar la constante comstackda), se extiende fácilmente a una entrada más complicada, y no requiere tratar con la gramática ( compile tomar cualquier cosa arbitrariamente complicada y reducirla) a una secuencia de instrucciones simples).

Puede usar el módulo ast y escribir un NodeVisitor que verifique que el tipo de cada nodo sea parte de una lista blanca.

 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, "", "eval"), {'__builtins__': None}, locals) except Exception: raise ValueError(expr) 

Debido a que funciona a través de una lista blanca en lugar de una lista negra, es seguro. Las únicas funciones y variables a las que puede acceder son aquellas a las que explícitamente le da acceso. Rellené un dictado con funciones relacionadas con las matemáticas para que pueda proporcionar acceso a ellas fácilmente si lo desea, pero tiene que usarlo explícitamente.

Si la cadena intenta llamar a funciones que no se han proporcionado o invocar algún método, se generará una excepción y no se ejecutará.

Debido a que utiliza el analizador y el evaluador integrados de Python, también hereda las reglas de promoción y precedencia de Python.

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

El código anterior solo se ha probado en Python 3.

Si lo desea, puede agregar un decorador de tiempo de espera en esta función.

Esta es una respuesta masivamente tardía, pero creo que es útil para futuras referencias. En lugar de escribir su propio analizador matemático (aunque el ejemplo de creación de párrafos anterior es excelente), puede usar SymPy. No tengo mucha experiencia con él, pero contiene un motor matemático mucho más potente que el que cualquiera pueda escribir para una aplicación específica y la evaluación de la expresión básica es muy fácil:

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

Muy bien hecho! A from sympy import * trae mucho más soporte de funciones, como funciones trigonométricas, funciones especiales, etc., pero lo he evitado aquí para mostrar lo que viene de dónde.

Creo que usaría eval() , pero primero verificaría que la cadena sea una expresión matemática válida, en lugar de algo malicioso. Podrías usar una expresión regular para la validación.

eval() también toma argumentos adicionales que puede usar para restringir el espacio de nombres en el que opera para mayor seguridad.

[Sé que esta es una pregunta antigua, pero vale la pena señalar nuevas soluciones útiles a medida que aparecen]

Desde python3.6, esta capacidad ahora está incorporada en el lenguaje , acuñado “f-strings” .

Ver: PEP 498 – Interpolación de cuerdas literales

Por ejemplo (note el prefijo f ):

 f'{2**4}' => '16' 

Si no desea utilizar eval, la única solución es implementar el analizador de gramática adecuado. Echa un vistazo a pyparsing .

Si ya estás usando wolftwiglpha, tienen una api de python, que te permite evaluar expresiones. Puede ser un poco lento, pero al menos muy preciso.

https://pypi.python.org/pypi/wolftwiglpha

Aquí está mi solución al problema sin usar eval. Funciona con Python2 y Python3. No funciona con números negativos.

 $ 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]) ) 

solucion.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) 

Utilice eval en un espacio de nombres limpio:

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

El espacio de nombres limpio debe evitar la inyección. Por ejemplo:

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

De lo contrario obtendrías:

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

Es posible que desee dar acceso al módulo de matemáticas:

 >>> import math >>> ns = vars(math).copy() >>> ns['__builtins__'] = None >>> eval('cos(pi/3)', ns) 0.50000000000000011 

Python ya tiene una función para evaluar de forma segura cadenas que contienen expresiones literales:

http://docs.python.org/2/library/ast.html#ast.literal_eval