Глава 23 · Приложение

Домашний проект A: калькулятор с Tkinter

Экран, сетка кнопок и безопасный разбор выражения через ast — без единого вызова eval().

Классический калькулятор на Tkinter: экран сверху, сетка кнопок с цифрами и знаками снизу — тот же приём, что и в тренажёре «Крестики-нолики» из главы 19, одна функция на все кнопки:

calculator.py
KNOPKI = [
    ("7", 1, 0), ("8", 1, 1), ("9", 1, 2), ("/", 1, 3),
    ("4", 2, 0), ("5", 2, 1), ("6", 2, 2), ("*", 2, 3),
    # ...
]

for podpis, stroka, stolbec in KNOPKI:
    knopka = tk.Button(root, text=podpis,
                        command=lambda s=podpis: na_cifru_ili_znak_nazhali(s))
    knopka.grid(row=stroka, column=stolbec, sticky="we")

Вычисление выражения без eval()

Обычный eval() выполняет любой переданный ему код Python — даже если строка формально похожа на арифметику, ограничить его до безопасного набора операций сложнее, чем кажется. Калькулятор разбирает выражение через модуль ast и вычисляет только то, что действительно разрешено: числа, + - * / и скобки:

calculator.py
def vychislit_uzel(uzel: ast.AST) -> float:
    if isinstance(uzel, ast.Constant) and isinstance(uzel.value, int | float):
        return uzel.value
    if isinstance(uzel, ast.BinOp) and type(uzel.op) in DOPUSTIMYE_OPERATORY:
        return DOPUSTIMYE_OPERATORY[type(uzel.op)](
            vychislit_uzel(uzel.left), vychislit_uzel(uzel.right)
        )
    if isinstance(uzel, ast.UnaryOp) and isinstance(uzel.op, ast.USub):
        return -vychislit_uzel(uzel.operand)
    raise ValueError(f"недопустимый узел выражения: {ast.dump(uzel)}")

def vychislit_vyrazhenie(vyrazhenie: str) -> str:
    derevo = ast.parse(vyrazhenie, mode="eval")
    return str(vychislit_uzel(derevo.body))
Почему это безопаснее eval(), даже ограниченного
Строка вроде "import os" не является допустимым арифметическим выражением, поэтому ast.parse(..., mode="eval") сразу вызывает SyntaxError — разбору просто негде появиться. Дерево vychislit_uzel() понимает только числа и четыре арифметических оператора: вызвать функцию, обратиться к переменной или выполнить любой другой код через него в принципе невозможно, а не запрещено проверкой символов.

Логика отдельно от интерфейса

Состояние калькулятора — текущее введённое выражение — обычная строка внутри маленького класса без единого упоминания Tkinter. Импорт файла calculator.py не открывает окно: tk.Tk() создаётся только внутри main(), поэтому логику можно проверять тестами без графического интерфейса вовсе.

Полный код: projects/tkinter/calculator/calculator.py

Задание

★★ Самостоятельная задача
Кнопка «±»

Добавьте кнопку, меняющую знак текущего результата, не трогая функцию vychislit_vyrazhenie().

GitHubGitHub-практика

Заведите Issue «Add calculator project» с критериями готовности (интерфейс открывается, арифметика работает, деление на ноль обрабатывается, тесты проходят), создайте ветку feat/calculator, добавьте проект в python-mini-projects и завершите работу через Pull Request.

Практика: вычисления и последовательности нажатий
Модуль tkinter открывает нативное окно Python — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →