Reputation: 139
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?
Upvotes: 12
Views: 17814
Reputation: 595
One hack I found that worked is compiling the expression and looking at co_consts
of the resulting code:
>>> compile("2*37982+(32<<4)-297^80", "<expr>", "eval").co_consts
(76227,)
>>> compile("__import__('subprocess').run(['firefox'])", "<expr>", "eval").co_consts
('subprocess', 'firefox')
The downsides of this are that multiplication only works with integers if they fit in a 128-bit signed int; and that it's a side effect of compiler optimizations.
>>> compile("170141183460469231731687303715884105728*1", "<expr>", "eval").co_consts
(170141183460469231731687303715884105728, 1)
>>> compile("170141183460469231731687303715884105727*1", "<expr>", "eval").co_consts
(170141183460469231731687303715884105727,)
Addition (and substraction) always works though.
Alternatively (if this is too limiting), try something like this, which only runs the expression if it only contains a subset of opcodes.
Upvotes: 0
Reputation: 2183
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
Upvotes: 1
Reputation: 304
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
Upvotes: 3
Reputation: 988
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.
Upvotes: 4
Reputation: 43543
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
Upvotes: 6
Reputation: 152820
One way would be to use numexpr. It's mostly a module for optimizing (and multithreading) numpy 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.
Upvotes: 16