Как сделать калькулятор для бота

if message.startswith("!число"):
    split = message.split(' ', 1)
    a = []
    b = []
    plus = split[1].split('+')
    a.append(plus[0])
    b.append(plus[1])
    print(int(a[0]) + int(b[0]))

Вот так я сделал операцию сложения. Пользователь пишет в чат команду !число 5+5 и бот отвечает 10. Но если пользователь напишет там - или * или /, то программа выдаст ошибку, как можно сделать это лучше, что бы пользователь мог сделать любую арифметическую операцию?


Ответы (3 шт):

Автор решения: Aziz Umarov

Ребята вы изобретаете велосипед при чем не самым лучшим образом.

Воспользуйтесь функцией eval() благо есть такая.

UPD. в целях дальнейшего улучшения ответа.

Функция eval() действительно является опасной если давать ей что угодно. Статей и публикаций можно найти не мало. Но я всё же хотел бы акцентировать внимание на то что вы программист и дозволительное пользователям всё в ваших руках. Говорить что не существует безопасного сценария использования eval(), я считаю не аргументированным поскольку дать прямой доступ к eval() из приложения это как дать обезьяне гранату. Дать гранату можно преждевременно обезвредив и убедившись в безопасности данного мероприятия.

PS Элементарно требуйте ввода строго арифметического выражения: цифры и операторы (/*-+). Просто не пойму почему для данного мероприятия нужно подключать третью библиотеку и прочее. Решается простым RegEx для валидации выражения, никаких функций, вызовов, обращения к переменным и прочей ереси.

Функции под eval можно ограничить. Например, будет доступна только abs

eval('abs(-3)', {"__builtins__":{"abs":abs}})

, но другие функции можно будет достать по ссылке:

 eval('abs.__self__.bin(1)', {"__builtins__":{'abs':abs}})

Валидация для eval необходима, даже если убрать все функции и останутся только операторы.

→ Ссылка
Автор решения: gil9red

Предлагаю вам калькулятор из сторонней библиотеки.

Использовать калькулятор так:

# pip install simpleeval
from simpleeval import simple_eval

...

if message.startswith("!число"):
    _, math_str = message.split(' ', 1)
    result = simple_eval(math_str)
    print(result)

Пример калькулятора через ast:

from simpleeval import simple_eval

print(simple_eval("21 + 21"))  # 42
print(simple_eval("2 + 2 * 2"))  # 6
print(simple_eval("21 + 19 / 7 + (8 % 3) ** 9"))  # 535.7142857142857
print(simple_eval("square(11)", functions={"square": lambda x: x * x}))  # 121

PS.

Еще примеры калькуляторов, среди них есть и те, что не нужно будет устанавливать

→ Ссылка
Автор решения: MaxLevs

Калькуляторы - вещь весёлая, заманчиво "простая", и, на проверку, очень глубокая. По сути - первый шаг в мир компиляторов и парсеров.

Есть много вариантов, как распарсить выражение и вычислить его, много разных грамматик и видов парсеров (LL, LR, PEG, etc.).

Этот процесс обычно состоит из нескольких этапов:

  1. Разбор на лексемы
  2. Построение дерева парсинга
  3. Построение абстрактного синтаксического дерева (из кусков текста выражения)
  4. Построение конкретного синтаксического дерева (из самописных объектов операций и числовых объектов)
  5. Вычисление, обходом конкретного синтаксического дерева

Написание и отладка кода, реализующего этот процесс, могут занять приличное время, но он этого зависит, как гибко ты сможешь расширять грамматику.

Однако, если ограничиваться простыми операциями, тебе не нужен полноценный парсинг - достаточно будет обойтись "упрощенным" вариантом, в котором смешаны сразу и парсинг, и построение дерева, и вычисление. Такой подход усложняет процесс расширения грамматики, однако не перегружает код лишними описаниями и позволяет, написав несколько функций, реализовать вычисление выражений.

Для написания примера такого упрощенного парсера-калькулятора создадим простую грамматику и напишем для неё функции-парсеры.

# num -> /^[+-]?\d+(\.\d+)?/
# group -> ( term )
# value -> num | group
# mul -> num [*/] mul
# mul -> num
# sum -> mul [+-] sum
# sum -> mul
# term -> sum

import re


def num(expr):
    expr = expr.lstrip()
    res = re.match("^[+-]?\d(\.\d+)?", expr)
    if res:
        return float(res.group(0)), expr[res.end():]
    else:
        return None, expr

def value(expr):
    res, rest = num(expr)
    if res != None:
        return res, rest
    res, rest = grouping(expr)
    return res, rest

def grouping(expr):
    expr = expr.lstrip()
    rest = ""
    if expr[0] == "(":
        rest = expr[1:]
    else:
        return None, expr
    numb, rest = term(rest)
    if rest[0] != ")":
        return None, expr
    return numb, rest[1:]

def mul_oper(expr):
    expr = expr.lstrip()
    res = re.match("[*/]", expr)
    if res:
        return res.group(0), expr[res.end():]
    else:
        return None, expr

def mul(expr):
    numb1, rest1 = value(expr)

    if numb1 == None:
        return None, expr

    op, rest2 = mul_oper(rest1)

    if op == None:
        return numb1, rest1

    numb2, rest2 = mul(rest2)

    if op == "*":
        return numb1 * numb2, rest2
    if op == "/":
        return numb1 / numb2, rest2

    return None, expr

def sum_oper(expr):
    expr = expr.lstrip()
    res = re.match("[+-]", expr)
    if res:
        return res.group(0), expr[res.end():]
    else:
        return None, expr

def sum(expr):
    numb1, rest1 = mul(expr)

    if numb1 == None:
        return None, expr

    op, rest2 = sum_oper(rest1)

    if op == None:
        return numb1, rest1

    numb2, rest2 = sum(rest2)

    if op == "+":
        return numb1 + numb2, rest2
    if op == "-":
        return numb1 - numb2, rest2

    return None, expr

def term(expr):
    return sum(expr)

print(term("(2 + 2) * 2"))

Здесь функция term() занимается разбором выражения целиком, функции sum() и mul() - разбором выражений сложения/вычитания и умножения/деления с учетом приоритета, функция grouping() занимается разбором выражений в скобках, а num() - разбором чисел. Это урезанная реализация PEG.

После прохождения каждой функции изменяется не только получаемый результат, но и состояние парсера (переменная rest). В случае, если одна из ветвей парсинга провалилась (не был встречен ожидаемый символ), мы по цепочке подымимся до ближайшей альтернативы (backtracking). Это можно увидеть в работе функции value(), а так же (хоть и скрыто) в функциях mul() и sum().


Можно написать и чуть более прокаченный вариант с чуть большим погружением в PEG. Отличия будут заключаться в следующем:

  1. Будут добавлены новые функции для построения грамматики: две терминальные и три нетерминальные (комбинаторы, их видов больше, но нам хватит и трёх).
  2. Правила, написанные "вручную" будут переписаны как комбинация более простых правил
  3. Логика выбора пути разбора будет перенесена в комбинаторы

Терминалы:

  1. Токен - строгое совпадение набора символов - token()
  2. Регулярное выражение - совпадение по шаблону - rexpr()

Комбинаторы:

  1. Последовательность (или конкатенация) - serial()
  2. Альтернатива - alternative()
  3. Optional - независимо от удачного парсинга возвращает удачу (удача или неудача отличается состоянием переменной rest) - optional()
import re

def GN(func, *args):
    def exec(text: str):
        return func(text, *args)
    return exec

def token(text: str, token_text: str):
    if text.startswith(token_text):
        return token_text, text[len(token_text):]
    return None, text

def rexpr(text: str, regex: str):
    text = text.lstrip()
    res = re.match(regex, text)
    if res:
        return res.group(0), text[res.end():]
    else:
        return None, text

def serial(text, *funcs):
    res, rest = [], text
    for func in funcs:
        resd, restd = func(rest)
        if resd is None:
            return None, text
        res.append(resd)
        rest = restd
    if len(res) == 0:
        return None, text
    return res, rest

def alternative(text, *funcs):
    for func in funcs:
        res, rest = func(text)
        if res is not None:
            return res, rest
    return None, text

def optional(text, func):
    res, rest = func(text)
    return [res], rest

def num(expr):
    res, rest = rexpr(expr, r"^[+-]?\d(\.\d+)?")
    if res is not None:
        return float(res), rest
    return res, rest

def value(expr):
    sign = GN(rexpr, r"[+-]")
    maybe_sign = GN(optional,  sign)
    val = GN(alternative, num, grouping)
    res, rest = serial(expr, maybe_sign, val)

    if res is None:
        return None, expr

    numb = res[1]
    if res[0][0] == "-":
        return -numb, rest
    return numb, rest

def grouping(expr):
    opened_bracket = GN(token, "(")  # token(text, "(")
    closed_bracket = GN(token, ")")  # token(text, ")")

    res, rest = serial(expr, opened_bracket, term, closed_bracket)

    if res is None:
        return None, expr
    return res[1], rest

def mul(expr):
    full_expr = GN(serial, value, GN(rexpr, r"[*/]"), mul)
    res, rest = alternative(expr, full_expr, value)

    if res is None:
        return None, expr

    if isinstance(res, float):
        return res, rest

    numb1 = res[0]
    op = res[1]
    numb2 = res[2]

    if op == "*":
        return numb1 * numb2, rest
    if op == "/":
        return numb1 / numb2, rest

    return None, expr

def sum(expr):
    full_expr = GN(serial, mul, GN(rexpr, r"[+-]"), sum)
    res, rest = alternative(expr, full_expr, mul)

    if res is None:
        return None, expr

    if isinstance(res, float):
        return res, rest

    numb1 = res[0]
    op = res[1]
    numb2 = res[2]

    if op == "+":
        return numb1 + numb2, rest
    if op == "-":
        return numb1 - numb2, rest

    return None, expr

def term(expr):
    return sum(expr)


print(term("2 + 2 * 2"))

Для того, чтобы комбинаторы работали как надо, необходим общий интерфейс. При написании в ООП стиле достаточно создать для каждого объекта правила метод execute(text: str). Однако для функций такой интерфейс необходимо создать отдельно: каждая функция помимо текста принимает и другие параметры, такие как, например, правила для rexpr или функции для serial.

Функция GN(rule, \*args) создаёт такую обёртку, возвращая функцию text: str -> Union[Tuple[None, str], Tuple[float, str]]

→ Ссылка