Преобразование строки в Python выражение (программа падает при слишком больших числах)

Рейтинг: 2Ответов: 2Опубликовано: 11.01.2023

Есть пользователь который вводит выражение типа "100 + 100 / 10", которое необходимо привести в некое число преобразовав в питоновское выражение.

Для парсинга использую py_expression_eval:

from py_expression_eval import Parser

parser = Parser()
parser.parse("100 + 100 / 10").evaluate({})

Пишу команду !math для Discord бота на hikari & hikari-lightbulb.

Проблема в том, что при вводе выражения типа 100000000000000000 ^ 100000000000000000 бот ложится не выбрасывая абсолютно никаких ошибок.

Пробовал ещё numexpr, но тоже самое.

С чем может быть связано? Как обрабатывать такие "краш-числа"?

UPD: Да не нужно мне эти числа вычислять! Мне нужно защитить программу от ввода пользователя.

Ответы

▲ 3

Как я уже написал в комментариях, вычисление таких степеней очень затратно и интерпретатор питона на них просто подвисает. Вот вам пример, что с этим можно сделать с помощью таймаут-декоратора, который нужно предварительно установить (их на самом деле много разных написано, я выбрал первый попавшийся в поиске):

pip install timeout-decorator

Ну и библиотеку парсинга выражений заодно напишу как ставить для полной воспроизводимости кода:

pip install py_expression_eval

Этим кодом я покажу, какое время считаются разные степени и какой длины получаются числа в результате этих вычислений, ну и как использовать декоратор таймаута:

import sys
import math
import time
import timeout_decorator
from py_expression_eval import Parser

# без увеличения этого значения вычисление выражений падает
sys.set_int_max_str_digits(1_000_000_000)

@timeout_decorator.timeout(5)
def mycalc(n):
    parser = Parser()
    return parser.parse(n).evaluate({})

def mytest():
    try:
        for i in range(10):
            t1 = time.perf_counter()
            n = 10 ** i
            s = f"{n} ^ {n}"
            x = mycalc(s)
            t2 = time.perf_counter()
            t = t2-t1
            l = 1 + int(math.log10(x))
            print(f'Шаг: {i}, Вычисление: {s:^20}, Время: {t:.6f}, Получено цифр: {l}', flush=True)
    except TimeoutError:
        print(f'Шаг: {i}, Вычисление: {s:^20}, Превышено допустимое время!')

if __name__ == '__main__':
    mytest()

Результат работы кода:

Шаг: 0, Вычисление:        1 ^ 1        , Время: 0.007594, Получено цифр: 1
Шаг: 1, Вычисление:       10 ^ 10       , Время: 0.000222, Получено цифр: 11
Шаг: 2, Вычисление:      100 ^ 100      , Время: 0.000238, Получено цифр: 201
Шаг: 3, Вычисление:     1000 ^ 1000     , Время: 0.000255, Получено цифр: 3001
Шаг: 4, Вычисление:    10000 ^ 10000    , Время: 0.003241, Получено цифр: 40001
Шаг: 5, Вычисление:   100000 ^ 100000   , Время: 0.095435, Получено цифр: 500001
Шаг: 6, Вычисление:  1000000 ^ 1000000  , Время: 4.649145, Получено цифр: 6000001
Шаг: 7, Вычисление: 10000000 ^ 10000000 , Превышено допустимое время!

Количество цифр я считаю не через len(str(x)), а через десятичный логарифм, потому что этот подсчёт без установки параметра int_max_str_digits вообще падает, а если его поставить побольше, как у меня и сделано (для работы библиотеки парсинга выражений), то уже это вычисление начинает работать очень долго, поэтому пришлось через логарифм, но логарифм всё-равно даёт тот же результат, только моментально, я проверял на небольших значениях переменной цикла, да и просто по логике.

▲ 3

Доставьте память в компьютер, чтобы не было свопа.

Сколько памяти потребуется чтобы сохранить 100000000000000000100000000000000000 ?

Питон хранит длинные целые в виде цепочек из 30-битных слов. Каждое слово занимает 4 байта. Число n в таком представлении занимает 4·log230(n) байт. Вычислим место :

4·log230(100000000000000000100000000000000000) =

= 4·ln(100000000000000000100000000000000000) / ln(230) =

= 4·100000000000000000·ln(100000000000000000) / ln(230) ≈

≈ 4·1017·39.14 / 20.79 байт ≈ 7.53·1017 байт = 753 петабайт

Архитектуру компьютера менять не надо, 64 разрядов достаточно чтобы адресовать такое количество памяти (264 ≈ 1.64·1019 > 7.53·1017). Память придётся докупить. При текущих ценах на память - 21 миллиард рублей за 5.5 миллионов планок по 128 гигабайт каждая.