python-3.x 简单字符串方程的安全求值

d4so4syb  于 2023-05-02  发布在  Python
关注(0)|答案(5)|浏览(111)

我在写一个程序,其中一个方程作为字符串输入,然后计算。到目前为止,我已经得出了这个结论:

test_24_string = str(input("Enter your answer: "))
test_24 = eval(test_24_string)

我需要这个方程的字符串版本和计算版本。然而,eval是一个非常危险的函数。但是,使用int()是行不通的,因为它是一个等式。有没有一个Python函数可以从一个字符串中计算一个数学表达式,就像输入一个数字一样?

iklwldmw

iklwldmw1#

一种方法是使用numexpr。它主要是一个优化(和多线程)numpy操作的模块,但它也可以处理数学Python表达式:

>>> import numexpr
>>> numexpr.evaluate('2 + 4.1 * 3')
array(14.299999999999999)

你可以在结果上调用.item来获得一个类似Python的类型:

>>> numexpr.evaluate('17 / 3').item()
5.666666666666667

它是一个第三方扩展模块,所以它可能是完全矫枉过正,但它肯定比eval更安全,并支持相当多的功能(包括numpymath操作)。如果还支持“变量替换”:

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

我在这里复制了包含支持类型的部分文档:
安全计算表达式节点或包含Python文本或容器显示的字符串。提供的字符串或节点只能由以下Python文字结构组成:字符串、字节、数字、元组、列表、字典、集合、布尔值和None。

1sbrub3j

1sbrub3j2#

编写后缀表达式计算器并不困难。下面是一个工作示例。(在github上也有available。)

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
xvw2m8pv

xvw2m8pv3#

我遇到了同样的问题,并解决了这个问题:

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)

我看不出来里面可能还有安全问题。如果有安全问题,请告诉我。

guz6ccqo

guz6ccqo4#

我这么做是为了回答同样的问题。它很容易适应。

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
c6ubokkw

c6ubokkw5#

公认的答案是不正确的。numexpr.evaluate依赖于eval。请参阅https://github.com/pydata/numexpr/issues/323了解如何在用户输入上使用此库可能出错的信息。
相反,这里是一个由Paul McGuire编写的用于算术表达式的eval免费计算器:https://github.com/pyparsing/pyparsing/blob/master/examples/fourFn.py .艰苦的工作已经完成了。如果您将以下代码片段添加到示例代码的当前形式,您将拥有一个能够进行算术运算的safe_eval函数:

def safe_eval(expression: str) -> float:
    BNF().parseString(expression, parseAll=True)
    return evaluate_stack(exprStack[:])

请注意,Paul的示例代码旨在演示如何使用解析器,而不是提供一个算术API,因此您可能希望稍微修改一下代码以符合您的约定。标签:Safe way to parse user-supplied mathematical formula in Python

相关问题