Python 计算器 - 使用十进制模块评估语句字符串

Python calculator - evaluating string of statements using decimal module

我正在编写一个 Discord 机器人,它将接受用户作为字符串的输入,然后计算其中的表达式,没有什么特别的,只是简单的算术运算。我有两个问题 - 安全和小数。首先我使用了 simpleeval 包,它工作正常但它有小数问题,例如 0.1 + 0.1 + 0.1 - 0.3 会 return 5.551115123125783e-17。经过大量谷歌搜索后,我找到了一个有效的答案,但它使用内置的 eval() 函数,显然使用它是一个很大的问题。

是否有 better/safer 处理方法?这实现了 https://docs.python.org/3/library/tokenize.html#examples decistmt() 方法,该方法用小数代替语句字符串中的浮点数。但最后我使用了 eval() 并且通过所有这些检查我仍然不确定它是否安全并且我宁愿避免它。

这就是 decistmt() 的作用:

from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO

def decistmt(s):
    """Substitute Decimals for floats in a string of statements.

    >>> from decimal import Decimal
    >>> s = 'print(+21.3e-5*-.1234/81.7)'
    >>> decistmt(s)
    "print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"

    The format of the exponent is inherited from the platform C library.
    Known cases are "e-007" (Windows) and "e-07" (not Windows).  Since
    we're only showing 12 digits, and the 13th isn't close to 5, the
    rest of the output should be platform-independent.

    >>> exec(s)  #doctest: +ELLIPSIS
    -3.21716034272e-0...7

    Output from calculations with Decimal should be identical across all
    platforms.

    >>> exec(decistmt(s))
    -3.217160342717258261933904529E-7
    """
    result = []
    g = tokenize(BytesIO(s.encode('utf-8')).readline)  # tokenize the string
    for toknum, tokval, _, _, _ in g:
        if toknum == NUMBER and '.' in tokval:  # replace NUMBER tokens
            result.extend([
                (NAME, 'Decimal'),
                (OP, '('),
                (STRING, repr(tokval)),
                (OP, ')')
            ])
        else:
            result.append((toknum, tokval))
    return untokenize(result).decode('utf-8')
 # example user input: "(20+5)^4 - 550 + 8"
@bot.command()
async def calc(context, *, user_input):
   
    #this is so user can use both ^ and ** for power and use "," and "." for decimals
    equation = user_input.replace('^', "**").replace(",", ".")
    valid_operators: list = ["+", "-", "/", "*", "%", "^", "**"]
    # checks if a string contains any element from a list, it will also return false if the iterable is empty, so this covers empty check too
    operator_check: bool = any(
        operator in equation for operator in valid_operators)
    # checks if arithmetic operator is last or first element in equation, to prevent doing something like ".calc 2+" or ".calc +2"

    def is_last_or_first(equation: str):
        for operator in valid_operators:
            if operator == equation[-1]:
                return True
            elif operator == equation[0]:
                if operator == "-":
                    return False
                else:
                    return True
    #isupper and islower checks whether there are letters in user input
    if not operator_check or is_last_or_first(equation) or equation.isupper() or equation.islower():
        return await context.send("Invalid input")

    result = eval(decistmt(equation))
    result = float(result)

    # returning user_input here so user sees "^" instead of "**"

    async def result_generator(result: int or float):
        await context.send(f'**Input:** ```fix\n{user_input}```**Result:** ```fix\n{result}```')
    # this is so if the result is .0 it returns an int
    if result.is_integer():
        await result_generator(int(result))
    else:
        await result_generator(result)

这是用户输入后发生的事情

user_input = "0.1 + 0.1 + 0.1 - 0.3"
float_to_decimal = decistmt(user_input) 
print(float_to_decimal)
print(type(float_to_decimal))
# Decimal ('0.1')+Decimal ('0.1')+Decimal ('0.1')-Decimal ('0.3')
# <class 'str'>

现在我需要评估这个输入,所以我使用 eval()。我的问题是 - 这安全吗(我假设不安全)还有其他方法可以评估 float_to_decimal?

编辑。根据要求,更深入的解释:

整个应用程序是一个聊天机器人。这个“计算”功能是用户可以使用的命令之一。它是通过在聊天中输入“.calc”来调用的,“.calc”是一个前缀,后面的任何内容都是参数,它将被连接成一个字符串,最终我将得到一个字符串作为参数。我执行了一系列检查以限制输入(删除字母等)。检查后,我留下了一个由数字、算术运算符和括号组成的字符串。我想评估该字符串的数学表达式。我将该字符串传递给 decistmt 函数,该函数将该字符串中的每个浮点数转换为 Decimal 对象,结果是一个看起来像这样的字符串:“Decimal ('2.5') + Decimal ('-5.2')”。现在我需要评估该字符串中的表达式。我为此使用了 simpleeval 模块,但它与 Decimal 模块不兼容,因此我正在使用内置的 eval() 方法进行评估。我的问题是,是否有一种更安全的方法来评估字符串中的数学表达式?

尝试按照@Shiva 的建议对数字使用 Decimal

还有 here's the answer from user @unutbu,它包括使用 PyParsing 库和自定义包装器来评估数学表达式。 在 Python 或系统表达式(例如 import <package>dir())的情况下将抛出 ParseException 错误。

pyparsing 最近的一个衍生包是 plusminus, a wrapper around pyparsing specifically for this case of embedded arithmetic evaluation. You can try it yourself at http://ptmcg.pythonanywhere.com/plusminus. Since plusminus uses its own parser, it is not subject to the common eval attacks, as Ned Batchelder describes in this still-timely blog post: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html。 Plusminus 还包括对表达式的处理,这些表达式虽然有效且“安全”,但非常耗时,可用于拒绝服务攻击 - 例如 "9**9**9".

我正计划对 plusminus 进行重构,这将稍微改变 API,但您可以按原样将其用于您的 Discord 插件。 (您会发现 plusminus 还支持一些超出为 Python 定义的标准运算符和函数的附加运算符和函数,例如 "|4-7|" 用于绝对值,或 "√42" 用于 sqrt(42) - 单击该页面上的示例按钮以获取更多支持的表达式。您还可以将值保存在变量中,然后在以后的表达式中使用该变量。(这可能不适用于您的插件,因为变量状态可能是共享的由多个 Discordian 编写。)

Plusminus 还设计用于支持子类化以定义新的运算符,例如将评估 "3d6+d20" 为“3 卷 6 面骰子,加上 1 卷 20 面骰子”的掷骰子",包括每次评估时的随机掷骰。