Open Source
Math Engine: интерпретатор выражений для Python без eval()
Проект
Созданный с нуля движок вычисления математических выражений, полностью без eval() из Python: токенизатор, парсер рекурсивного спуска, точная Decimal-арифметика и посимвольная диагностика ошибок. Теперь опубликован на PyPI, 399 тестов и 90 % покрытие.
Вычислить число, которое приходит в виде текста, например 3 + 4 * 2 из поля формы, файла конфигурации или API, звучит как одна строка на Python: eval("3 + 4 * 2"). Именно эта строка и есть проблема. С помощью Math Engine я с нуля построил полноценный движок вычислений, который считает математические выражения безопасно, точно и наглядно, полностью без Python-функции eval(). С этой недели он опубликован как версия 0.6.6 на PyPI.
Почему не просто eval()
eval() выполняет произвольный код на Python. Замаскированный под число ввод вроде __import__('os').system(...) будет выполнен без единого вопроса. Для любого приложения, которое принимает выражения из файла, формы или API, eval() тем самым не калькулятор, а открытый путь к выполнению кода.
К этому добавляются два более тихих дефекта. Во-первых, корректность: Python-тип float считает в двоичной системе, 0.1 + 0.2 даёт 0.30000000000000004. Для финансовой формулы или учебного контекста это не почти правильно, а неверно. Во-вторых, диагностика: сломанное выражение возвращает Python-трейсбек на внутреннем номере строки, а не то место во входной строке, где действительно сидит проблема.
Итак, задачей был движок, который никогда не выполняет чужой код, считает точно, а не приближённо, и локализует каждую ошибку с точностью до символа. И всё это в библиотечном качестве: с тестами, документацией, версионированием и установкой через pip.
Безопасно, потому что иначе и не может
Движок нигде не вызывает eval(), exec() или compile(). Это не фильтр, добавленный задним числом, а сама архитектура. Каждый ввод проходит через замкнутый конвейер:
Input → Tokenizer → Parser → Evaluator → Formatter → Output
Алфавит этого конвейера конечен: числа, операторы, скобки и фиксированный список разрешённых имён функций. Враждебный ввод в худшем случае может вызвать типизированную ошибку, но никогда не выполнить код. Это та гарантия безопасности, которую основанное на eval() решение в принципе дать не может.
Как парсер понимает приоритет
Умножение перед сложением, скобки в первую очередь: приоритет операторов в Math Engine не вколочен через таблицу, а закодирован как структура. Парсер рекурсивного спуска вкладывает друг в друга десять уровней приоритета, от присваивания через побитовые операторы и сложение до возведения в степень. Левоассоциативен оператор или правоассоциативен, следует из этой структуры само собой: a - b - c читается как (a - b) - c, а 2 ** 3 ** 2, наоборот, связывается вправо, ровно как в Python.
Осознанное решение при этом: ^, это побитовый XOR, а не возведение в степень. Возводят в степень через **, ровно как в C и Python, чтобы движок вёл себя так, как ожидает программист.
Точно вместо почти правильно
Каждое число от первой до последней ступени, это decimal.Decimal, никогда не float. Поэтому 0.1 + 0.2 здесь в точности равно 0.3. Точность вычислений определяется заново для каждого вычисления, от 100 до 10 000 знаков в зависимости от ввода, с жёстким верхним пределом в 20 000 знаков. Так длинный результат никогда не обрезается молча, а короткий не занимает память без необходимости.
Ошибки, которые указывают на символ
Параллельно токенам токенизатор ведёт для каждого спан: начальный столбец, конечный столбец, оригинальный текст. Эта позиция проходит через всё синтаксическое дерево и в конце висит на каждой ошибке. Результат, это диагностика, которая не сообщает «синтаксическая ошибка где-то», а указывает на виновный символ.
Те же позиционные данные обслуживают два режима, переключаемых через единственную настройку. Для библиотеки типизированная ошибка распространяется к вызывающему коду, с кодом и позицией для перехвата. Для консоли движок ловит сам и рисует указатель ровно под ошибочным местом:
12 + * 3
^
здесь ожидалось значение, а не оператор
Под этим лежит каталогизированная система из 78 четырёхзначных кодов ошибок в девяти семействах. Цифры структурированы: код 3008 означает «Calculator, ядро парсера, более одной точки в числе». Эти коды осознанно никогда не перенумеровываются, они являются контрактом перед интерфейсом и перед внешними парсерами логов.
Больше, чем калькулятор
На том же синтаксическом дереве сидят ещё две возможности. Если выражение содержит = и переменную, движок решает линейное уравнение символически, вместо того чтобы гадать, включая чисто именованные особые случаи для «нет решения» и «бесконечно много решений». А для низкоуровневой арифметики есть режим Programmer's Calculator с фиксированной разрядностью слова от 8 до 64 бит, дополнительным кодом и побитовыми операторами, так что 127 + 1 в 8-битном режиме корректно переполняется в -128.
Проверено, версионировано, поставлено
Безопасный движок, которому нельзя доверять, бесполезен. За примерно 4 200 строками продакшн-кода поэтому стоит набор из 399 pytest-тестов при 90% покрытии. Собственный хелпер проверяет не только то, что выражение завершается ошибкой, но и то, что оно завершается ошибкой с точным кодом ошибки на точной позиции символа. GitHub Actions прогоняет полный набор при каждом push против пяти версий Python, от 3.8 до 3.12.
Поставляется всё это как чистый Python-wheel всего с двумя зависимостями и тремя консольными командами, включая интерактивный REPL с историей и автодополнением по Tab. Шесть релизов примерно за пять месяцев, сквозь и по правилам Semantic Versioning.
Попробовать самому
Math Engine открыт под лицензией MIT и устанавливается в одну строку:
pip install math-engine
Код лежит на GitHub, пакет на PyPI. Та же инженерная дисциплина, которая здесь несёт опубликованную библиотеку, заложена и в клиентских проектах: безопасная обработка выражений без eval() для DSL, редакторов формул и rule-движков.
Избранные проекты
Оптимизация расходов AWS
Снижение расходов на AWS на 65 % (с $3 850 до $1 330 в месяц) за счёт безопасного вывода legacy-платформы из эксплуатации — без простоев.
Реверс-инжиниринг и миграция legacy-БД
1,47 млн деталей извлечены из защищённой паролем БД производителя объёмом 1,2 ГБ и перенесены в новую систему клиента — включая 82 076 конвертированных схем, ноль нарушений, полностью аудируемо.
Book Lister AI
Настольное приложение: сканирует подержанные книги меньше чем за 30 секунд, распознаёт через Gemini Vision, устанавливает цену в реальном времени и публикует на eBay — +400 % к производительности.