Как сделать калькулятор для бота
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 шт):
Ребята вы изобретаете велосипед при чем не самым лучшим образом.
Воспользуйтесь функцией eval() благо есть такая.
UPD. в целях дальнейшего улучшения ответа.
Функция eval() действительно является опасной если давать ей что угодно. Статей и публикаций можно найти не мало. Но я всё же хотел бы акцентировать внимание на то что вы программист и дозволительное пользователям всё в ваших руках. Говорить что не существует безопасного сценария использования eval(), я считаю не аргументированным поскольку дать прямой доступ к eval() из приложения это как дать обезьяне гранату. Дать гранату можно преждевременно обезвредив и убедившись в безопасности данного мероприятия.
PS Элементарно требуйте ввода строго арифметического выражения: цифры и операторы (/*-+). Просто не пойму почему для данного мероприятия нужно подключать третью библиотеку и прочее. Решается простым RegEx для валидации выражения, никаких функций, вызовов, обращения к переменным и прочей ереси.
Функции под eval можно ограничить. Например, будет доступна только abs
eval('abs(-3)', {"__builtins__":{"abs":abs}})
, но другие функции можно будет достать по ссылке:
eval('abs.__self__.bin(1)', {"__builtins__":{'abs':abs}})
Валидация для eval необходима, даже если убрать все функции и останутся только операторы.
Предлагаю вам калькулятор из сторонней библиотеки.
Использовать калькулятор так:
# 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.
Еще примеры калькуляторов, среди них есть и те, что не нужно будет устанавливать
Калькуляторы - вещь весёлая, заманчиво "простая", и, на проверку, очень глубокая. По сути - первый шаг в мир компиляторов и парсеров.
Есть много вариантов, как распарсить выражение и вычислить его, много разных грамматик и видов парсеров (LL, LR, PEG, etc.).
Этот процесс обычно состоит из нескольких этапов:
- Разбор на лексемы
- Построение дерева парсинга
- Построение абстрактного синтаксического дерева (из кусков текста выражения)
- Построение конкретного синтаксического дерева (из самописных объектов операций и числовых объектов)
- Вычисление, обходом конкретного синтаксического дерева
Написание и отладка кода, реализующего этот процесс, могут занять приличное время, но он этого зависит, как гибко ты сможешь расширять грамматику.
Однако, если ограничиваться простыми операциями, тебе не нужен полноценный парсинг - достаточно будет обойтись "упрощенным" вариантом, в котором смешаны сразу и парсинг, и построение дерева, и вычисление. Такой подход усложняет процесс расширения грамматики, однако не перегружает код лишними описаниями и позволяет, написав несколько функций, реализовать вычисление выражений.
Для написания примера такого упрощенного парсера-калькулятора создадим простую грамматику и напишем для неё функции-парсеры.
# 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. Отличия будут заключаться в следующем:
- Будут добавлены новые функции для построения грамматики: две терминальные и три нетерминальные (комбинаторы, их видов больше, но нам хватит и трёх).
- Правила, написанные "вручную" будут переписаны как комбинация более простых правил
- Логика выбора пути разбора будет перенесена в комбинаторы
Терминалы:
- Токен - строгое совпадение набора символов -
token() - Регулярное выражение - совпадение по шаблону -
rexpr()
Комбинаторы:
- Последовательность (или конкатенация) -
serial() - Альтернатива -
alternative() - 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]]