Домашний проект A: калькулятор с Tkinter
Экран, сетка кнопок и безопасный разбор выражения через ast — без единого вызова eval().
Классический калькулятор на Tkinter: экран сверху, сетка кнопок с цифрами и знаками снизу — тот же приём, что и в тренажёре «Крестики-нолики» из главы 19, одна функция на все кнопки:
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 и вычисляет только то, что действительно разрешено:
числа, + - * / и скобки:
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))
"import os" не является допустимым арифметическим выражением, поэтому ast.parse(..., mode="eval") сразу вызывает SyntaxError — разбору просто негде появиться. Дерево vychislit_uzel() понимает только числа и четыре арифметических оператора: вызвать функцию, обратиться к переменной или выполнить любой другой код через него в принципе невозможно, а не запрещено проверкой символов.Логика отдельно от интерфейса
Состояние калькулятора — текущее введённое выражение — обычная строка внутри
маленького класса без единого упоминания Tkinter. Импорт файла
calculator.py не открывает окно: tk.Tk() создаётся
только внутри main(), поэтому логику можно проверять тестами
без графического интерфейса вовсе.
Полный код: projects/tkinter/calculator/calculator.py
Задание
Добавьте кнопку, меняющую знак текущего результата, не трогая функцию vychislit_vyrazhenie().
Заведите Issue «Add calculator project» с критериями готовности (интерфейс
открывается, арифметика работает, деление на ноль обрабатывается, тесты проходят),
создайте ветку feat/calculator, добавьте проект в
python-mini-projects и завершите работу через Pull Request.