安全地评估简单的字符串方程
Safely evaluate simple string equation
我正在编写一个程序,其中将方程作为字符串输入,然后计算。到目前为止,我想出了这个:
test_24_string = str(input("Enter your answer: "))
test_24 = eval(test_24_string)
我需要这个等式的字符串版本和计算版本。但是,eval
是一个非常危险的函数。但是,使用 int()
不起作用,因为它是一个等式。是否有 Python 函数可以像输入数字一样从字符串计算数学表达式?
一种方法是使用 numexpr. It's mostly a module for optimizing (and multithreading) numpy 运算,但它也可以处理数学 python 表达式:
>>> import numexpr
>>> numexpr.evaluate('2 + 4.1 * 3')
array(14.299999999999999)
您可以对结果调用 .item
以获得类似 python 的类型:
>>> numexpr.evaluate('17 / 3').item()
5.666666666666667
它是第 3 方扩展模块,所以在这里可能有点矫枉过正,但它绝对比 eval
更安全,并且支持相当多的功能(包括 numpy
和 math
操作) .如果还支持 "variable substitution":
>>> b = 10
>>> numexpr.evaluate('exp(17) / b').item()
2415495.27535753
python 标准库的一种方法,尽管非常有限,但 ast.literal_eval
。它适用于 Python:
中最基本的数据类型和文字
>>> import ast
>>> ast.literal_eval('1+2')
3
但使用更复杂的表达式失败,例如:
>>> ast.literal_eval('import os')
SyntaxError: invalid syntax
>>> ast.literal_eval('exec(1+2)')
ValueError: malformed node or string: <_ast.Call object at 0x0000023BDEADB400>
不幸的是,除了 +
和 -
之外的任何运算符都是不可能的:
>>> ast.literal_eval('1.2 * 2.3')
ValueError: malformed node or string: <_ast.BinOp object at 0x0000023BDEF24B70>
我在此处复制了部分文档,其中包含受支持的类型:
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.
编写后缀表达式计算器并不难。下面是一个工作示例。 (还有 available 在 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]
用法:
In [2]: from postfix import postfix
In [3]: postfix('1 2 + 7 /')
Out[3]: 0.42857142857142855
In [4]: 3/7
Out[4]: 0.42857142857142855
我遇到了同样的问题并解决了这个问题:
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)
那里可能仍然存在我看不到的安全问题。
如果有安全问题请告诉我。
我这样做是因为我需要回答同样的问题。很容易适应。
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'))
测试与使用
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
我正在编写一个程序,其中将方程作为字符串输入,然后计算。到目前为止,我想出了这个:
test_24_string = str(input("Enter your answer: "))
test_24 = eval(test_24_string)
我需要这个等式的字符串版本和计算版本。但是,eval
是一个非常危险的函数。但是,使用 int()
不起作用,因为它是一个等式。是否有 Python 函数可以像输入数字一样从字符串计算数学表达式?
一种方法是使用 numexpr. It's mostly a module for optimizing (and multithreading) numpy 运算,但它也可以处理数学 python 表达式:
>>> import numexpr
>>> numexpr.evaluate('2 + 4.1 * 3')
array(14.299999999999999)
您可以对结果调用 .item
以获得类似 python 的类型:
>>> numexpr.evaluate('17 / 3').item()
5.666666666666667
它是第 3 方扩展模块,所以在这里可能有点矫枉过正,但它绝对比 eval
更安全,并且支持相当多的功能(包括 numpy
和 math
操作) .如果还支持 "variable substitution":
>>> b = 10
>>> numexpr.evaluate('exp(17) / b').item()
2415495.27535753
python 标准库的一种方法,尽管非常有限,但 ast.literal_eval
。它适用于 Python:
>>> import ast
>>> ast.literal_eval('1+2')
3
但使用更复杂的表达式失败,例如:
>>> ast.literal_eval('import os')
SyntaxError: invalid syntax
>>> ast.literal_eval('exec(1+2)')
ValueError: malformed node or string: <_ast.Call object at 0x0000023BDEADB400>
不幸的是,除了 +
和 -
之外的任何运算符都是不可能的:
>>> ast.literal_eval('1.2 * 2.3')
ValueError: malformed node or string: <_ast.BinOp object at 0x0000023BDEF24B70>
我在此处复制了部分文档,其中包含受支持的类型:
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.
编写后缀表达式计算器并不难。下面是一个工作示例。 (还有 available 在 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]
用法:
In [2]: from postfix import postfix
In [3]: postfix('1 2 + 7 /')
Out[3]: 0.42857142857142855
In [4]: 3/7
Out[4]: 0.42857142857142855
我遇到了同样的问题并解决了这个问题:
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)
那里可能仍然存在我看不到的安全问题。 如果有安全问题请告诉我。
我这样做是因为我需要回答同样的问题。很容易适应。
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'))
测试与使用
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