计算字符串中的数学表达式

Evaluating a mathematical expression in a string

提问人:Pieter 提问时间:3/3/2010 更新时间:11/22/2023 访问量:170689

问:

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

这将返回以下错误:

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

我知道这可以解决这个问题,但是难道没有更好、更重要的是更安全的方法来评估存储在字符串中的数学表达式吗?eval

Python 数学

评论

9赞 kgiannakakis 3/3/2010
^ 是 XOR 运算符。预期值为 6。你可能想要 pow(2,4)。
38赞 fortran 3/3/2010
或更多 pythonally 2**4
1赞 kgiannakakis 3/3/2010
如果您不想使用 eval,那么唯一的解决方案是实现适当的语法解析器。看一看 pyparsing
0赞 K. Stopa 10/6/2020
对于简单的操作,您可以查看此代码 github.com/louisfisch/mathematical-expressions-parser
0赞 Amin 1/28/2022
要么应该采用 @fortran 的方法,要么需要有自己的自定义运算符解析器和计算器。

答:

6赞 Tim Goodman 3/3/2010 #1

我想我会使用 ,但会首先检查以确保字符串是有效的数学表达式,而不是恶意的。您可以使用正则表达式进行验证。eval()

eval()还采用其他参数,您可以使用这些参数来限制它在其中运行的命名空间,以提高安全性。

评论

3赞 High Performance Mark 3/3/2010
但是,当然,不要依赖正则表达式来验证任意数学表达式。
0赞 Tim Goodman 3/3/2010
@High-Performance Mark:是的,我想这取决于他想到什么样的数学表达式。例如,只是简单的算术,包括数字和,,,,,,或更复杂的东西+-*/**()
0赞 High Performance Mark 3/4/2010
@Tim -- 这是我担心的 (),或者更确切地说是 ((((((()))))))。事实上,我认为 OP 应该担心他们,我对 OP 的问题皱起了眉头。
3赞 jfs 3/5/2012
如果您不控制输入,即使您限制命名空间,例如 eval(“9**9**9**9**9**9**9**9**9”, {'__builtins__': None}) 也会消耗 CPU、内存,请不要使用。eval()
4赞 Antti Haapala -- Слава Україні 3/4/2016
限制 eval 的命名空间不会增加安全性
132赞 unutbu 3/3/2010 #2

Pyparsing 可用于解析数学表达式。具体而言,fourFn.py 演示了如何解析基本的算术表达式。下面,我将 fourFn 重新包装到一个数值解析器类中,以便于重用。

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

你可以像这样使用它

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

result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
1赞 krawyoti 3/3/2010 #3

在干净的命名空间中使用:eval

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

干净的命名空间应该防止注入。例如:

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

否则,您将获得:

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

您可能希望授予对数学模块的访问权限:

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

评论

39赞 Perkins 8/22/2014
eval(“(1).__class__.__bases__[0].__subclasses__()[81]('echo got through'.split())”,{'builtins':None}) #escapes 沙箱中
7赞 Antti Haapala -- Слава Україні 3/4/2016
Python 3.4:执行 bourne shell...eval("""[i for i in (1).__class__.__bases__[0].__subclasses__() if i.__name__.endswith('BuiltinImporter')][0]().load_module('sys').modules['sys'].modules['os'].system('/bin/sh')""", {'__builtins__': None})
12赞 user 3/4/2016
这是不安全的。恶意代码仍然可以执行。
0赞 Hannu 12/10/2017
This is not safe- 好吧,我认为它与整体上使用 bash 一样安全。顺便说一句:<——如上所述,“数学”是必需的。eval('math.sqrt(2.0)')
4赞 andybuckley 3/5/2012 #4

这是一个非常迟到的回复,但我认为对将来参考很有用。与其编写自己的数学解析器(尽管上面的 pyparsing 示例很棒),不如使用 SymPy。我没有太多的经验,但它包含一个比任何人都可能为特定应用程序编写的更强大的数学引擎,并且基本的表达式计算非常简单:

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

确实很酷!A 带来了更多的函数支持,例如触发函数、特殊函数等,但我在这里避免了这一点,以显示来自哪里的内容。from sympy import *

评论

3赞 Mark Mikofski 8/7/2013
sympy “安全”吗?似乎有许多帖子表明它是 eval() 的包装器,可以以同样的方式利用。也不接受 numpy ndarrays。evalf
15赞 Mark Mikofski 8/7/2013
对于不受信任的输入,没有符号是不安全的。试试我传递的这个调用,而不是.在其他计算机上,索引可能会有所不同。这是 Ned Batchelder 漏洞利用的变体sympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""")subprocess.Popen()lsrm -rf /
1赞 Antti Haapala -- Слава Україні 3/4/2016
事实上,它根本没有增加安全性。
260赞 jfs 3/5/2012 #5

eval是邪恶的

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

注意:即使您使用 set to,它仍然可以使用内省来爆发:__builtins__None

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

使用计算算术表达式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)

您可以轻松限制每个操作或任何中间结果的允许范围,例如,限制以下各项的输入参数: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

或者限制中间结果的量级:

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

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

评论

49赞 Daniel Fairhead 12/4/2013
很酷的帖子,谢谢。我采用了这个概念,并试图制作一个应该易于使用的库:github.com/danthedeckie/simpleeval
0赞 Hotschke 7/30/2014
这可以扩展到 的功能吗?import math
4赞 Antti Haapala -- Слава Україні 4/29/2016
请注意,这是不安全的。例如,解释器崩溃。ast.parseast.parse('()' * 1000000, '<string>', 'single')
1赞 jfs 4/29/2016
@AnttiHaapala很好的例子。这是 Python 解释器中的错误吗?无论如何,大输入的处理是微不足道的,例如,使用 .if len(expr) > 10000: raise ValueError
2赞 jfs 4/29/2016
@AnttiHaapala您能否提供一个无法使用检查修复的示例?或者你的观点是 Python 实现中存在错误,因此通常不可能编写安全代码?len(expr)
14赞 Mark Mikofski 8/7/2013 #6

sympy.sympify().evalf()* 的一些更安全的替代品:eval()

*根据文档中的以下警告,SymPy sympify 也是不安全的。

警告:请注意,此函数使用 ,因此不应用于未经审查的输入。eval

11赞 Perkins 8/22/2014 #7

好的,所以 eval 的问题在于它可以很容易地逃脱它的沙盒,即使你摆脱了 .所有逃离沙箱的方法都归结为使用或(通过运算符)通过一些允许的对象(或类似对象)获取对某些危险对象的引用。 通过设置为 来消除。 是困难的,因为它不能简单地删除,既因为它是不可变的,也因为删除它会破坏一切。但是,只能通过操作员访问,因此从输入中清除它足以确保 eval 无法逃逸其沙盒。
在处理公式时,小数的唯一有效用法是当它之前或之后是 时,因此我们只需删除 的所有其他实例。
__builtins__getattrobject.__getattribute__.''.__class__.__bases__[0].__subclasses__getattr__builtins__Noneobject.__getattribute__object__getattribute__.[0-9].

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

请注意,虽然 python 通常将其视为 ,但这将删除尾随并留下 .您可以将 ,和添加到允许遵循的列表中,但何必呢?1 + 1.1 + 1.0.1 + 1)EOF.

评论

0赞 djvg 4/4/2018
可以在此处找到具有有趣讨论的相关问题。
4赞 kaya3 12/15/2019
无论目前关于删除的论点是否正确,如果未来的 Python 版本引入新的语法,允许以其他方式访问不安全的对象或函数,这就有可能出现安全漏洞。此解决方案在 Python 3.6 中已经不安全,因为 f 字符串允许以下攻击: .基于白名单而不是黑名单的解决方案会更安全,但实际上最好完全不解决这个问题。.f"{eval('()' + chr(46) + '__class__')}"eval
0赞 Perkins 12/15/2019
这是关于未来语言功能引入新安全问题的一个很好的观点。
0赞 Perkins 3/12/2022
也就是说,f-strings 目前不会破坏这一点。F-Strings 是围绕显式字符串格式调用的语法糖,该调用在 .py 文件中编译为多步字节码。虽然有可能以某种方式利用,但尝试将 f-string 作为上述 eval 函数的输入会因键错误而死亡,因为它依赖于函数调用来获取 .我希望利用 python 的 unicode 库来获取“.”的可能性更大。.
9赞 Kevin 5/29/2015 #8

您可以使用 ast 模块并编写一个 NodeVisitor 来验证每个节点的类型是否属于白名单。

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)

因为它通过白名单而不是黑名单工作,所以它是安全的。它唯一可以访问的函数和变量是您显式授予它访问权限的函数和变量。我用与数学相关的函数填充了一个字典,这样你就可以根据需要轻松地提供对这些函数的访问,但你必须显式使用它。

如果字符串尝试调用尚未提供的函数或调用任何方法,则将引发异常,并且不会执行。

因为它使用了 Python 内置的解析器和赋值器,所以它还继承了 Python 的优先级和提升规则。

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

以上代码仅在 Python 3 上进行了测试。

如果需要,您可以在此函数上添加超时装饰器。

评论

0赞 Kevin 4/29/2023
回想起来,以这种方式使用总比(滥用)使用要好。只是在评论中提到这一点,因为我不确定事后以这种方式更改答案的礼仪。ast.walkast.NodeVisitor
12赞 Perkins 3/11/2016 #9

之所以如此危险,是因为默认函数将为任何有效的 python 表达式生成字节码,而默认的 or 将执行任何有效的 python 字节码。到目前为止,所有的答案都集中在限制可以生成的字节码(通过清理输入)或使用 AST 构建自己的领域特定语言。evalexeccompileevalexec

相反,您可以轻松创建一个简单的函数,该函数无法执行任何恶意操作,并且可以轻松地对使用的内存或时间进行运行时检查。当然,如果是简单的数学,那么就有捷径可走。eval

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]

其工作方式很简单,任何常数数学表达式在编译过程中都会被安全地计算并存储为常数。编译返回的代码对象由 ,它是 的字节码,后跟要加载的常量的编号(通常是列表中的最后一个),后跟 ,是 的字节码。如果此快捷方式不起作用,则表示用户输入不是常量表达式(包含变量或函数调用或类似内容)。dLOAD_CONSTSRETURN_VALUE

这也为一些更复杂的输入格式打开了大门。例如:

stringExp = "1 + cos(2)"

这需要实际评估字节码,这仍然非常简单。Python 字节码是一种面向堆栈的语言,因此一切都是简单或类似的问题。关键是只实现安全的操作码(加载/存储值、数学运算、返回值),而不是不安全的操作码(属性查找)。如果您希望用户能够调用函数(不使用上面的快捷方式的全部原因),只需将实现设置为仅允许“安全”列表中的函数即可。TOS=stack.pop(); op(TOS); stack.put(TOS)CALL_FUNCTION

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

显然,它的实际版本会更长一些(有 119 个操作码,其中 24 个与数学相关)。添加和其他几个将允许输入类似或类似,很容易。它甚至可以用来执行用户创建的函数,只要用户创建的函数本身是通过 VMeval 执行的(不要让它们可调用!!或者它们可以在某个地方用作回调)。处理循环需要对字节码的支持,这意味着从迭代器更改为当前指令并维护指向当前指令的指针,但并不难。对于DOS的抵抗力,主循环应该检查自计算开始以来已经过去了多少时间,并且某些运算符应该拒绝超过某个合理限制的输入(这是最明显的)。STORE_FAST'x=5;return x+xgotoforwhileBINARY_POWER

虽然这种方法比简单表达式的简单语法解析器要长一些(参见上文关于仅获取编译常量的信息),但它很容易扩展到更复杂的输入,并且不需要处理语法(将任何任意复杂的东西简化为一系列简单的指令)。compile

评论

0赞 MestreLion 1/29/2021
感谢这个惊人的快捷方式!但是有一个错误,至少在 Python 3.6 中是这样:总是计算为 ,因为奇怪的是,计算为 ,而不是 。解决方法是使用或c.co_code[0]==b'd'Falseb'foo'[0]intbytesc.co_code[0:1]==b'd'chr(c.co_code[0])=='d'
0赞 MestreLion 1/29/2021
在尝试使快捷方式起作用时,我偶然发现了其他问题,因此我创建了一个适用于 Python 3.6+ 的函数,并试图涵盖一些极端情况: stackoverflow.com/a/65945969/624066
0赞 Perkins 1/30/2021
我差不多 5 年前写了这篇文章......我什至不记得我的目标是哪个 python 版本,很可能是 2.7。更快的比较是 .我几乎从来不需要数学输入,所以第二种方法是我实际继续使用的。c.co_code[0]==100
6赞 shx2 12/17/2016 #10

[我知道这是一个老问题,但值得指出的是,当它们出现时,新的有用解决方案]

从 python3.6 开始,这个功能现在内置在语言中,创造了“f-strings”。

请参见: PEP 498 -- 文字字符串插值

例如(注意 f 前缀):

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

评论

9赞 Bernhard 1/19/2017
非常有趣的链接。但我想 f 字符串是为了使编写源代码更容易,而问题似乎是关于在变量内部处理字符串(可能来自不受信任的来源)。在这种情况下,不能使用 f 字符串。
0赞 Skyler 8/14/2017
有没有办法做一些事情来达到 f'{2{operator}4}' 的效果,您现在可以分配运算符来执行 2+4 或 2*4 或 2-4 等
5赞 kaya3 12/15/2019
这实际上等同于只是做,所以它肯定不比 更安全。str(eval(...))eval
0赞 ART GALLERY 4/3/2019 #11

这是我在不使用 eval 的情况下解决问题的方法。适用于 Python2 和 Python3。它不适用于负数。

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

solution.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)
7赞 MestreLion 1/29/2021 #12

基于 Perkins 的惊人方法,我更新并改进了他用于简单代数表达式(无函数或变量)的“快捷方式”。现在它适用于 Python 3.6+ 并避免了一些陷阱:

import re

# Kept outside simple_eval() just for performance
_re_simple_eval = re.compile(rb'd([\x00-\xFF]+)S\x00')

def simple_eval(expr):
    try:
        c = compile(expr, 'userinput', 'eval')
    except SyntaxError:
        raise ValueError(f"Malformed expression: {expr}")
    m = _re_simple_eval.fullmatch(c.co_code)
    if not m:
        raise ValueError(f"Not a simple algebraic expression: {expr}")
    try:
        return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
    except IndexError:
        raise ValueError(f"Expression not evaluated as constant: {expr}")

测试,使用其他答案中的一些示例:

for expr, res in (
    ('2^4',                         6      ),
    ('2**4',                       16      ),
    ('1 + 2*3**(4^5) / (6 + -7)',  -5.0    ),
    ('7 + 9 * (2 << 2)',           79      ),
    ('6 // 2 + 0.0',                3.0    ),
    ('2+3',                         5      ),
    ('6+4/2*2',                    10.0    ),
    ('3+2.45/8',                    3.30625),
    ('3**3*3/3+3',                 30.0    ),
):
    result = simple_eval(expr)
    ok = (result == res and type(result) == type(res))
    print("{} {} = {}".format("OK!" if ok else "FAIL!", expr, result))
OK! 2^4 = 6
OK! 2**4 = 16
OK! 1 + 2*3**(4^5) / (6 + -7) = -5.0
OK! 7 + 9 * (2 << 2) = 79
OK! 6 // 2 + 0.0 = 3.0
OK! 2+3 = 5
OK! 6+4/2*2 = 10.0
OK! 3+2.45/8 = 3.30625
OK! 3**3*3/3+3 = 30.0

测试错误的输入:

for expr in (
    'foo bar',
    'print("hi")',
    '2*x',
    'lambda: 10',
    '2**1234',
):
    try:
        result = simple_eval(expr)
    except ValueError as e:
        print(e)
        continue
    print("OK!")  # will never happen
Malformed expression: foo bar
Not a simple algebraic expression: print("hi")
Expression not evaluated as constant: 2*x
Expression not evaluated as constant: lambda: 10
Expression not evaluated as constant: 2**1234

评论

2赞 Perkins 1/30/2021
这太好了。比我的方法干净得多。请注意,作为输入馈送会导致未经处理的异常,因为正则表达式会找到 return 语句,但它与常量 return 不匹配。它有几种解决方案(寻找操作码,检测太长)。另外值得注意的是,这仍然容易受到 DoS 攻击(需要外部缓解)。lambda: 10MAKE_FUNCTIONLOAD_CONST
1赞 a_guest 1/27/2022
值得注意的是,这依赖于这样一个事实,即无论使用什么 Python 实现,它都必须实现常量折叠(诚然,流行的实现就是这样)。但是,并非所有常量表达式都以这种方式进行优化,例如,在运行时保持原样进行计算。在这种情况下,函数会遇到 .2**12345IndexError: tuple index out of range
0赞 MestreLion 1/30/2022
谢谢@a_guest!我已经更新了解决方案以抓住这种情况!
1赞 miorey 5/12/2021 #13

使用 lark 解析器库

from operator import add, sub, mul, truediv, neg, pow
from lark import Lark, Transformer, v_args

calc_grammar = f"""
    ?start: sum
    ?sum: product
        | sum "+" product   -> {add.__name__}
        | sum "-" product   -> {sub.__name__}
    ?product: power
        | product "*" power  -> {mul.__name__}
        | product "/" power  -> {truediv.__name__}
    ?power: atom
        | power "^" atom -> {pow.__name__}
    ?atom: NUMBER           -> number
         | "-" atom         -> {neg.__name__}
         | "(" sum ")"

    %import common.NUMBER
    %import common.WS_INLINE

    %ignore WS_INLINE
"""


@v_args(inline=True)
class CalculateTree(Transformer):
    add = add
    sub = sub
    neg = neg
    mul = mul
    truediv = truediv
    pow = pow
    number = float


calc_parser = Lark(calc_grammar, parser="lalr", transformer=CalculateTree())
calc = calc_parser.parse


def eval_expr(expression: str) -> float:
    return calc(expression)


print(eval_expr("2^4"))
print(eval_expr("-1*2^4"))
print(eval_expr("-2^3 + 1"))
print(eval_expr("2**4"))  # Error

0赞 Elias Mi 1/23/2022 #14

我来这里也是在寻找一个数学表达式解析器。通过阅读一些答案并查找库,我遇到了我现在正在使用的 py-expression。它基本上可以处理很多运算符和公式构造,但如果你缺少一些东西,你可以轻松地向它添加新的运算符/函数。

基本语法为:

from py_expression.core import Exp
exp = Exp()

parsed_formula = exp.parse('a+4')

result = exp.eval(parsed_formula, {"a":2})

到目前为止,我遇到的唯一问题是它没有内置的数学常数,也没有添加它们的机制。然而,我只是提出了一个解决方案:https://github.com/FlavioLionelRita/py-expression/issues/7

0赞 Vrtulka23 11/22/2023 #15

如何实现针对您的特定问题量身定制的自己的表达式求解器。使用 scinumtools 表达式求解器,您可以使用预先存在的运算符,也可以从头开始创建自己的运算符。

from scinumtools.solver import *

with ExpressionSolver(AtomBase) as es:
    es.solve("sin(23) < 1 && 3*2 == 6 || !(23 > 43) && cos(0) == 1")

下面是一个如何构建自己的运算符的小示例:

class OperatorSquare(OperatorBase):   # operate from left side
    symbol: str = '~'
    def operate_unary(self, tokens):
        right = tokens.get_right()
        tokens.put_left(right*right)
class OperatorCube(OperatorBase):     # operate from right side
    symbol: str = '^'
    def operate_unary(self, tokens):
        left = tokens.get_left()
        tokens.put_left(left*left*left)
operators ={'square':OperatorSquare,'cube':OperatorCube,'add':OperatorAdd}
steps = [
    dict(operators=['square','cube'], otype=Otype.UNARY),
    dict(operators=['add'],           otype=Otype.BINARY),
]
with ExpressionSolver(AtomBase, operators, steps) as es:
    es.solve('~3 + 2^')
# will result in: Atom(17)

你甚至可以改变原子的行为:

class AtomCustom(AtomBase):
    value: str
    def __init__(self, value:str):
        self.value = str(value)
    def __add__(self, other):
        return AtomCustom(self.value + other.value)
    def __gt__(self, other):
        return AtomCustom(len(self.value) > len(other.value))
operators = {'add':OperatorAdd,'gt':OperatorGt,'par':OperatorPar}
steps = [
    dict(operators=['par'],  otype=Otype.ARGS),
    dict(operators=['add'],  otype=Otype.BINARY),
    dict(operators=['gt'],   otype=Otype.BINARY),
]
with ExpressionSolver(AtomCustom, operators, steps) as es:
    es.solve("(limit + 100 km/s) > (limit + 50000000000 km/s)")
# will result in: Atom('False')

顺便说一句,我是作者,我很想听听您对scinumtools项目的意见和建议。在 GitHubPyPi 上查看