Примеры кода на Python

Калькулятор на Python: пошаговое руководство от простого к сложному

Калькулятор на Python — один из лучших проектов для начинающих программистов. Он позволяет на практике освоить ключевые концепции языка: переменные, условия, циклы, функции, обработку ошибок, ООП и даже графические интерфейсы. В этом руководстве мы пройдём путь от самого простого калькулятора в несколько строк до полноценного GUI-приложения с кнопками

Каждый пример — это полностью рабочий код, который можно скопировать и запустить. После каждого блока кода идёт подробный разбор: что делает каждая строка и почему она написана именно так. Статья рассчитана на тех, кто только начинает изучать Python, но последние разделы будут полезны и более опытным разработчикам.

Что вам понадобится: Python 3.10 или новее (для примера с match/case; остальные примеры работают на Python 3.6+). Установить Python можно с официального сайта python.org

Простой калькулятор на Python (if/elif)

Начнём с самого базового варианта. Этот калькулятор на питоне запрашивает два числа и операцию, а затем выводит результат. Никаких циклов, никаких функций — чистая линейная логика

# Простой калькулятор на Python
# Запрашиваем первое число
num1 = float(input("Введите первое число: "))

# Запрашиваем операцию
operation = input("Введите операцию (+, -, *, /): ")

# Запрашиваем второе число
num2 = float(input("Введите второе число: "))

# Выполняем вычисление в зависимости от операции
if operation == "+":
    result = num1 + num2
    print(f"{num1} + {num2} = {result}")
elif operation == "-":
    result = num1 - num2
    print(f"{num1} - {num2} = {result}")
elif operation == "*":
    result = num1 * num2
    print(f"{num1} * {num2} = {result}")
elif operation == "/":
    if num2 == 0:
        print("Ошибка: деление на ноль!")
    else:
        result = num1 / num2
        print(f"{num1} / {num2} = {result}")
else:
    print(f"Неизвестная операция: {operation}")

Разбор кода калькулятора на Python строка за строкой:

  • float(input(...)) — функция input() считывает ввод пользователя как строку, а float() преобразует эту строку в число с плавающей точкой. Мы используем float, а не int, чтобы можно было вводить дробные числа вроде 3.14
  • operation = input(...) — здесь мы считываем операцию как строку
  • Преобразовывать её не нужно, потому что +, -, *, / — это именно строки
  • Конструкция if/elif/else — это цепочка условий. Python проверяет их по порядку сверху вниз. Как только одно условие оказывается истинным, выполняется соответствующий блок, а остальные пропускаются.
  • f"{num1} + {num2} = {result}" — это f-строка (formatted string literal). Выражения в фигурных скобках {} заменяются значениями переменных. Это удобный и читаемый способ форматирования вывода
  • Внутри блока деления мы отдельно проверяем num2 == 0, чтобы программа не упала с ошибкой ZeroDivisionError
  • Блок else в конце обрабатывает случай, когда пользователь ввёл что-то непредусмотренное — например, % или букву

Этот код калькулятора на Python максимально прост, но у него есть существенный недостаток: после одного вычисления программа завершается. Чтобы посчитать снова, её нужно перезапускать. Исправим это в следующем примере

Калькулятор с циклом while

Добавим бесконечный цикл while True, чтобы калькулятор на питоне работал до тех пор, пока пользователь сам не решит выйти. Это делает программу гораздо удобнее в использовании

# Калькулятор на Python с циклом while
print("=== Калькулятор ===")
print("Для выхода введите 'выход' или 'q'\n")

while True:
    # Запрашиваем первое число (или команду выхода)
    user_input = input("Введите первое число: ")

    # Проверяем, хочет ли пользователь выйти
    if user_input.lower() in ("выход", "q", "quit", "exit"):
        print("До свидания!")
        break

    # Преобразуем ввод в число
    num1 = float(user_input)

    # Запрашиваем операцию
    operation = input("Введите операцию (+, -, *, /): ")

    # Запрашиваем второе число
    num2 = float(input("Введите второе число: "))

    # Выполняем вычисление
    if operation == "+":
        result = num1 + num2
    elif operation == "-":
        result = num1 - num2
    elif operation == "*":
        result = num1 * num2
    elif operation == "/":
        if num2 == 0:
            print("Ошибка: деление на ноль!\n")
            continue
        result = num1 / num2
    else:
        print(f"Неизвестная операция '{operation}'. Попробуйте снова.\n")
        continue

    print(f"Результат: {num1} {operation} {num2} = {result}\n")

Что нового в этом примере:

  • while True — создаёт бесконечный цикл. Программа будет выполняться снова и снова, пока мы не прервём цикл явно.
  • break — немедленно прерывает цикл while. Мы вызываем его, когда пользователь вводит «выход» или «q».
  • continue — пропускает оставшуюся часть текущей итерации цикла и начинает новую. Мы используем его при ошибках (деление на ноль, неизвестная операция), чтобы калькулятор не пытался вывести несуществующий результат, а просто предложил ввести данные заново.
  • user_input.lower() — метод lower() приводит строку к нижнему регистру. Это нужно, чтобы команда выхода работала независимо от того, написал пользователь «Выход», «ВЫХОД» или «выход».
  • Проверка in ("выход", "q", "quit", "exit") — оператор in проверяет, входит ли значение в кортеж допустимых вариантов. Так мы поддерживаем несколько команд выхода одновременно.

Программа стала удобнее, но код по-прежнему довольно «плоский». Вся логика находится в одном месте, и если мы захотим добавить новые операции, файл быстро станет нечитаемым. Пора разделить код на функции

Калькулятор с функциями

Разделение кода на функции — одна из важнейших практик в программировании. Принцип DRY (Don’t Repeat Yourself — «не повторяй себя») говорит: если какой-то фрагмент кода используется несколько раз, вынесите его в функцию. Это делает как сделать калькулятор в Python более чистым и поддерживаемым

# Калькулятор на Python с функциями

def add(a: float, b: float) -> float:
    """Сложение двух чисел."""
    return a + b

def subtract(a: float, b: float) -> float:
    """Вычитание двух чисел."""
    return a - b

def multiply(a: float, b: float) -> float:
    """Умножение двух чисел."""
    return a * b

def divide(a: float, b: float) -> float | None:
    """Деление двух чисел. Возвращает None при делении на ноль."""
    if b == 0:
        return None
    return a / b

def get_number(prompt: str) -> float | None:
    """Запрашивает число у пользователя. Возвращает None при ошибке ввода."""
    try:
        return float(input(prompt))
    except ValueError:
        return None

def calculate(num1: float, operation: str, num2: float) -> tuple[float | None, str]:
    """
    Выполняет вычисление и возвращает кортеж (результат, сообщение).
    Если произошла ошибка, результат будет None.
    """
    operations = {
        "+": add,
        "-": subtract,
        "*": multiply,
        "/": divide,
    }

    if operation not in operations:
        return None, f"Неизвестная операция: '{operation}'"

    result = operations[operation](num1, num2)

    if result is None and operation == "/":
        return None, "Ошибка: деление на ноль!"

    return result, f"{num1} {operation} {num2} = {result}"

def main():
    """Главная функция калькулятора."""
    print("=== Калькулятор на Python ===")
    print("Операции: +, -, *, /")
    print("Для выхода введите 'q'\n")

    while True:
        # Получаем первое число
        user_input = input("Первое число (или 'q' для выхода): ")
        if user_input.lower() == "q":
            print("До свидания!")
            break

        try:
            num1 = float(user_input)
        except ValueError:
            print("Это не число. Попробуйте снова.\n")
            continue

        # Получаем операцию
        operation = input("Операция (+, -, *, /): ").strip()

        # Получаем второе число
        num2 = get_number("Второе число: ")
        if num2 is None:
            print("Это не число. Попробуйте снова.\n")
            continue

        # Вычисляем и выводим результат
        result, message = calculate(num1, operation, num2)
        print(f"{message}\n")

# Точка входа в программу
if __name__ == "__main__":
    main()

Разбор ключевых изменений:

  • Отдельные функции для каждой операции (add, subtract, multiply, divide). Каждая делает ровно одну вещь. Это упрощает тестирование и отладку.
  • Словарь operations — вместо длинной цепочки if/elif мы используем словарь, где ключ — символ операции, а значение — ссылка на функцию. Запись operations[operation](num1, num2) вызывает нужную функцию по символу операции. Это элегантный паттерн, который часто используется в Python.
  • Функция get_number — инкапсулирует логику получения числа от пользователя с обработкой ошибки. Если пользователь введёт текст вместо числа, функция вернёт None, а не вызовет исключение.
  • Функция calculate — принимает два числа и операцию, возвращает кортеж из результата и текстового сообщения. Это разделяет логику вычисления и логику ввода-вывода.
  • if __name__ == "__main__" — стандартная конструкция Python. Код внутри этого блока выполняется только при прямом запуске файла, но не при его импорте как модуля. Это хорошая практика для любого скрипта.
  • Аннотации типов (a: float, -> float | None) — не влияют на работу программы, но служат документацией и помогают IDE подсвечивать ошибки. Синтаксис float | None доступен начиная с Python 3.10

Калькулятор с match/case (Python 3.10+)

В Python 3.10 появился оператор структурного сопоставления match/case — аналог switch из других языков, но значительно мощнее. Давайте перепишем калькулятор питон с его использованием

# Калькулятор на Python с match/case (требует Python 3.10+)
import sys

def calculate(num1: float, operation: str, num2: float) -> str:
    """Выполняет вычисление и возвращает строку с результатом."""
    match operation:
        case "+":
            result = num1 + num2
        case "-":
            result = num1 - num2
        case "*":
            result = num1 * num2
        case "/":
            if num2 == 0:
                return "Ошибка: деление на ноль!"
            result = num1 / num2
        case "**" | "^":
            result = num1 ** num2
        case "%":
            if num2 == 0:
                return "Ошибка: деление на ноль при взятии остатка!"
            result = num1 % num2
        case "//":
            if num2 == 0:
                return "Ошибка: деление на ноль!"
            result = num1 // num2
        case _:
            return f"Неизвестная операция: '{operation}'"

    return f"{num1} {operation} {num2} = {result}"

def show_help():
    """Выводит справку по доступным операциям."""
    print("""
Доступные операции:
  +   — сложение
  -   — вычитание
  *   — умножение
  /   — деление
  **  — возведение в степень (также работает ^)
  %   — остаток от деления
  //  — целочисленное деление
  
Команды:
  help  — показать эту справку
  q     — выход из программы
""")

def main():
    """Главная функция калькулятора."""
    # Проверяем версию Python
    if sys.version_info < (3, 10):
        print("Этот калькулятор требует Python 3.10 или новее!")
        print(f"У вас: Python {sys.version_info.major}.{sys.version_info.minor}")
        sys.exit(1)

    print("=== Калькулятор (Python 3.10+) ===")
    print("Введите 'help' для справки, 'q' для выхода\n")

    while True:
        user_input = input(">>> ").strip()

        # Обрабатываем команды
        match user_input.lower():
            case "q" | "quit" | "exit" | "выход":
                print("До свидания!")
                break
            case "help" | "помощь" | "?":
                show_help()
                continue
            case "":
                continue

        # Пытаемся разобрать выражение: число операция число
        parts = user_input.split()

        if len(parts) != 3:
            print("Формат ввода: число операция число")
            print("Пример: 10 + 5\n")
            continue

        try:
            num1 = float(parts[0])
            operation = parts[1]
            num2 = float(parts[2])
        except ValueError:
            print("Ошибка: проверьте, что первое и третье значения — числа.\n")
            continue

        print(f"{calculate(num1, operation, num2)}\n")

if __name__ == "__main__":
    main()

Что нового в этом примере:

  • match/case — основа этого примера. Оператор match сравнивает значение переменной operation с паттернами, указанными после case. В отличие от switch в C или Java, здесь не нужен break — выполняется только один блок.
  • case "**" | "^" — символ | внутри case означает «ИЛИ». Этот паттерн сработает, если пользователь ввёл ** или ^. Удобно для поддержки нескольких вариантов одной операции.
  • case _ — подчёркивание является шаблоном-подстановкой (wildcard), который срабатывает, если ни один из предыдущих паттернов не подошёл. Это аналог else в if/elif/else или default в switch.
  • Новые операции — мы добавили возведение в степень (**), остаток от деления (%) и целочисленное деление (//).
  • Ввод в одну строку — теперь пользователь пишет 10 + 5, а не вводит каждое значение отдельно. Метод split() разбивает строку по пробелам на список.
  • sys.version_info — проверяем версию Python при запуске, чтобы дать понятное сообщение об ошибке вместо невнятного SyntaxError

Калькулятор с обработкой ошибок

Обработка ошибок — критически важная часть любой программы. В предыдущих примерах мы уже использовали try/except фрагментарно. Теперь создадим калькулятор на Python с полноценной защитой от всех типичных ошибок пользователя

# Калькулятор на Python с полной обработкой ошибок

class CalculatorError(Exception):
    """Базовое исключение для ошибок калькулятора."""
    pass

class InvalidOperationError(CalculatorError):
    """Ошибка: неизвестная операция."""
    pass

class DivisionByZeroError(CalculatorError):
    """Ошибка: деление на ноль."""
    pass

class InvalidInputError(CalculatorError):
    """Ошибка: некорректный ввод."""
    pass

# Словарь поддерживаемых операций
OPERATIONS: dict[str, str] = {
    "+": "сложение",
    "-": "вычитание",
    "*": "умножение",
    "/": "деление",
    "**": "возведение в степень",
    "%": "остаток от деления",
}

def parse_number(value: str) -> float:
    """
    Преобразует строку в число.
    Поддерживает запятую как десятичный разделитель.
    """
    # Заменяем запятую на точку (частая ошибка русскоязычных пользователей)
    value = value.strip().replace(",", ".")

    if not value:
        raise InvalidInputError("Пустой ввод. Введите число.")

    try:
        number = float(value)
    except ValueError:
        raise InvalidInputError(
            f"'{value}' — это не число. "
            f"Допустимы цифры, точка и знак минуса."
        )

    # Проверяем на специальные значения float
    if number != number:  # проверка на NaN
        raise InvalidInputError("Значение NaN не поддерживается.")
    if number == float("inf") or number == float("-inf"):
        raise InvalidInputError("Бесконечность не поддерживается.")

    return number

def safe_calculate(num1: float, operation: str, num2: float) -> float:
    """
    Выполняет безопасное вычисление.
    Выбрасывает CalculatorError при проблемах.
    """
    if operation not in OPERATIONS:
        available = ", ".join(OPERATIONS.keys())
        raise InvalidOperationError(
            f"Операция '{operation}' не поддерживается.\n"
            f"  Доступные операции: {available}"
        )

    # Проверки деления на ноль
    if operation in ("/", "%") and num2 == 0:
        raise DivisionByZeroError(
            "Деление на ноль невозможно. "
            "Второе число должно быть отличным от нуля."
        )

    # Защита от слишком больших степеней
    if operation == "**":
        if abs(num2) > 1000:
            raise CalculatorError(
                "Слишком большая степень (максимум: 1000). "
                "Вычисление может занять очень много времени."
            )

    # Выполняем вычисление
    match operation:
        case "+":
            return num1 + num2
        case "-":
            return num1 - num2
        case "*":
            return num1 * num2
        case "/":
            return num1 / num2
        case "**":
            return num1 ** num2
        case "%":
            return num1 % num2

def format_result(value: float) -> str:
    """Форматирует результат: убирает лишние нули у целых чисел."""
    if value == int(value) and abs(value) < 1e15:
        return str(int(value))
    return f"{value:.10g}"

def main():
    """Главная функция калькулятора с обработкой ошибок."""
    print("=== Калькулятор с обработкой ошибок ===")
    print("Формат: число операция число (например: 10 + 5)")
    print("Для выхода введите 'q'\n")

    while True:
        try:
            user_input = input(">>> ").strip()

            if user_input.lower() in ("q", "quit", "выход"):
                print("До свидания!")
                break

            if not user_input:
                continue

            # Разбираем ввод
            parts = user_input.split()

            if len(parts) != 3:
                raise InvalidInputError(
                    "Нужно ввести ровно три значения: число, операцию и число.\n"
                    "  Пример: 10 + 5"
                )

            # Парсим числа и операцию
            num1 = parse_number(parts[0])
            operation = parts[1]
            num2 = parse_number(parts[2])

            # Вычисляем
            result = safe_calculate(num1, operation, num2)
            formatted = format_result(result)
            print(f"  = {formatted}\n")

        except CalculatorError as e:
            # Наши ожидаемые ошибки — выводим понятное сообщение
            print(f"  ⚠ {e}\n")

        except KeyboardInterrupt:
            # Пользователь нажал Ctrl+C
            print("\n\nПрервано пользователем. До свидания!")
            break

        except EOFError:
            # Конец ввода (например, перенаправление из файла)
            print("\nКонец ввода. До свидания!")
            break

if __name__ == "__main__":
    main()

Подробный разбор обработки ошибок:

  • Собственная иерархия исключений. Мы создали базовый класс CalculatorError и три наследника. Это позволяет перехватывать все ошибки калькулятора одним блоком except CalculatorError, но при необходимости обрабатывать каждый тип отдельно.
  • parse_number — надёжная функция для преобразования ввода в число. Она обрабатывает: пустой ввод, текст вместо числа, запятую вместо точки (очень частая ошибка), а также специальные значения NaN и бесконечность.
  • Замена запятой на точку (value.replace(",", ".")) — в России принято использовать запятую как десятичный разделитель. Наш калькулятор это учитывает: ввод 3,14 будет корректно преобразован в 3.14.
  • Защита от больших степеней — выражение вроде 2 ** 1000000 может «подвесить» программу на долгое время. Мы ограничиваем показатель степени числом 1000.
  • format_result — убирает лишний .0 у целых результатов. Без неё 10 + 5 выводило бы 15.0 вместо 15.
  • KeyboardInterrupt — перехватывает нажатие Ctrl+C. Без этого обработчика при нажатии Ctrl+C пользователь увидит пугающий трейсбек.
  • EOFError — возникает, когда ввод заканчивается (например, при перенаправлении из файла: echo "10 + 5" | python calculator.py)

Продвинутый калькулятор с модулем math

Стандартная библиотека Python содержит модуль math с десятками математических функций. Используем его, чтобы сделать калькулятор в Python по-настоящему мощным: с квадратным корнем, тригонометрией, логарифмами и факториалом

# Продвинутый калькулятор на Python с модулем math
import math

def show_help():
    """Выводит справку по всем доступным операциям."""
    print("""
╔══════════════════════════════════════════════════════════╗
║           Продвинутый калькулятор — Справка              ║
╠══════════════════════════════════════════════════════════╣
║  Бинарные операции (формат: число операция число):       ║
║    +     сложение           -     вычитание              ║
║    *     умножение          /     деление                ║
║    **    степень             %     остаток от деления     ║
║    //    целочисленное деление                           ║
║                                                          ║
║  Унарные операции (формат: операция число):              ║
║    sqrt   квадратный корень   abs    модуль числа        ║
║    sin    синус (градусы)     cos    косинус (градусы)   ║
║    tan    тангенс (градусы)   log    логарифм (ln)      ║
║    log10  десятичный логарифм                            ║
║    fact   факториал           ceil   округление вверх    ║
║    floor  округление вниз     round  округление          ║
║                                                          ║
║  Константы:                                              ║
║    pi     число Пи            e      число Эйлера        ║
║                                                          ║
║  Команды:  help — справка,  q — выход                    ║
╚══════════════════════════════════════════════════════════╝
""")

def unary_operation(operation: str, value: float) -> float:
    """Выполняет унарную операцию над одним числом."""
    match operation:
        case "sqrt":
            if value < 0:
                raise ValueError("Квадратный корень из отрицательного числа не определён.")
            return math.sqrt(value)
        case "abs":
            return abs(value)
        case "sin":
            return math.sin(math.radians(value))
        case "cos":
            return math.cos(math.radians(value))
        case "tan":
            return math.tan(math.radians(value))
        case "log":
            if value <= 0:
                raise ValueError("Логарифм определён только для положительных чисел.")
            return math.log(value)
        case "log10":
            if value <= 0:
                raise ValueError("Логарифм определён только для положительных чисел.")
            return math.log10(value)
        case "fact":
            if value < 0 or value != int(value):
                raise ValueError("Факториал определён только для неотрицательных целых чисел.")
            if value > 170:
                raise ValueError("Факториал для чисел больше 170 слишком велик.")
            return float(math.factorial(int(value)))
        case "ceil":
            return float(math.ceil(value))
        case "floor":
            return float(math.floor(value))
        case "round":
            return float(round(value))
        case _:
            raise ValueError(f"Неизвестная операция: '{operation}'")

def binary_operation(num1: float, op: str, num2: float) -> float:
    """Выполняет бинарную операцию над двумя числами."""
    match op:
        case "+":
            return num1 + num2
        case "-":
            return num1 - num2
        case "*":
            return num1 * num2
        case "/":
            if num2 == 0:
                raise ZeroDivisionError("Деление на ноль!")
            return num1 / num2
        case "**":
            return num1 ** num2
        case "%":
            if num2 == 0:
                raise ZeroDivisionError("Деление на ноль при взятии остатка!")
            return num1 % num2
        case "//":
            if num2 == 0:
                raise ZeroDivisionError("Деление на ноль!")
            return num1 // num2
        case _:
            raise ValueError(f"Неизвестная операция: '{op}'")

def format_number(value: float) -> str:
    """Форматирует число для вывода."""
    if abs(value) < 1e-10:
        return "0"
    if value == int(value) and abs(value) < 1e15:
        return str(int(value))
    return f"{value:.10g}"

# Список унарных операций для определения типа ввода
UNARY_OPS = {"sqrt", "abs", "sin", "cos", "tan", "log", "log10", "fact", "ceil", "floor", "round"}

# Константы
CONSTANTS = {"pi": math.pi, "e": math.e}

def main():
    """Главная функция продвинутого калькулятора."""
    print("=== Продвинутый калькулятор ===")
    print("Введите 'help' для списка операций\n")

    # Хранение последнего результата
    last_result: float | None = None

    while True:
        try:
            raw = input(">>> ").strip()

            if not raw:
                continue

            command = raw.lower()

            if command in ("q", "quit", "выход"):
                print("До свидания!")
                break

            if command in ("help", "помощь", "?"):
                show_help()
                continue

            # Проверяем, не ввели ли константу
            if command in CONSTANTS:
                last_result = CONSTANTS[command]
                print(f"  = {format_number(last_result)}\n")
                continue

            parts = raw.split()

            # Подставляем последний результат вместо слова "ans"
            for i, part in enumerate(parts):
                if part.lower() == "ans":
                    if last_result is None:
                        raise ValueError("Нет предыдущего результата (ans).")
                    parts[i] = str(last_result)

            if len(parts) == 2:
                # Унарная операция: sqrt 16, sin 90, и т.д.
                op = parts[0].lower()
                value = float(parts[1].replace(",", "."))
                result = unary_operation(op, value)

            elif len(parts) == 3:
                # Бинарная операция: 10 + 5
                num1 = float(parts[0].replace(",", "."))
                op = parts[1]
                num2 = float(parts[2].replace(",", "."))
                result = binary_operation(num1, op, num2)

            else:
                print("Формат: число операция число  или  операция число")
                print("Пример: 10 + 5  или  sqrt 16\n")
                continue

            last_result = result
            print(f"  = {format_number(result)}\n")

        except (ValueError, ZeroDivisionError) as e:
            print(f"  Ошибка: {e}\n")
        except KeyboardInterrupt:
            print("\nДо свидания!")
            break

if __name__ == "__main__":
    main()

Ключевые возможности продвинутого калькулятора:

  • Унарные и бинарные операции. Калькулятор различает два формата ввода: sqrt 16 (одно число) и 10 + 5 (два числа). Определяется по количеству частей после split().
  • Модуль math. Используем math.sqrt для корня, math.sin/math.cos/math.tan для тригонометрии, math.log и math.log10 для логарифмов, math.factorial для факториала.
  • Тригонометрия в градусах. Функции math.sin и подобные работают в радианах, но обычные пользователи привыкли к градусам. Поэтому мы преобразуем ввод через math.radians().
  • Переменная ans — хранит последний результат. Можно написать ans * 2, чтобы удвоить предыдущий ответ. Это как кнопка Ans на обычном калькуляторе.
  • Константы pi и e — вводите просто pi или e, чтобы увидеть их значения. Их также можно использовать в вычислениях как ans

GUI-калькулятор на Tkinter

Теперь создадим графический калькулятор на питоне с настоящими кнопками. Мы используем библиотеку tkinter, которая входит в стандартную поставку Python. Важный момент: мы не используем eval() для вычисления выражений, потому что eval() может выполнить произвольный код и представляет угрозу безопасности. Вместо этого мы реализуем безопасный парсер выражений

# GUI-калькулятор на Python (Tkinter) — безопасная версия без eval()
import tkinter as tk
from tkinter import font as tkfont

class SafeCalculator:
    """
    Безопасный вычислитель арифметических выражений.
    Поддерживает: +, -, *, /, скобки, унарный минус.
    НЕ использует eval() — разбирает выражение вручную.
    """

    def __init__(self, expression: str):
        # Убираем пробелы и сохраняем выражение
        self.expression = expression.replace(" ", "")
        self.pos = 0  # текущая позиция при разборе

    def parse(self) -> float:
        """Основная точка входа — разбирает всё выражение."""
        result = self._parse_addition()
        if self.pos < len(self.expression):
            raise ValueError(
                f"Неожиданный символ: '{self.expression[self.pos]}'"
            )
        return result

    def _parse_addition(self) -> float:
        """Разбирает сложение и вычитание (низший приоритет)."""
        result = self._parse_multiplication()

        while self.pos < len(self.expression) and self.expression[self.pos] in "+-":
            op = self.expression[self.pos]
            self.pos += 1
            right = self._parse_multiplication()
            if op == "+":
                result += right
            else:
                result -= right

        return result

    def _parse_multiplication(self) -> float:
        """Разбирает умножение и деление (высший приоритет)."""
        result = self._parse_unary()

        while self.pos < len(self.expression) and self.expression[self.pos] in "*/":
            op = self.expression[self.pos]
            self.pos += 1
            right = self._parse_unary()
            if op == "*":
                result *= right
            else:
                if right == 0:
                    raise ZeroDivisionError("Деление на ноль")
                result /= right

        return result

    def _parse_unary(self) -> float:
        """Разбирает унарный минус и плюс."""
        if self.pos < len(self.expression):
            if self.expression[self.pos] == "-":
                self.pos += 1
                return -self._parse_unary()
            if self.expression[self.pos] == "+":
                self.pos += 1
                return self._parse_unary()

        return self._parse_primary()

    def _parse_primary(self) -> float:
        """Разбирает числа и выражения в скобках."""
        if self.pos >= len(self.expression):
            raise ValueError("Неожиданный конец выражения")

        # Скобки
        if self.expression[self.pos] == "(":
            self.pos += 1  # пропускаем '('
            result = self._parse_addition()
            if self.pos >= len(self.expression) or self.expression[self.pos] != ")":
                raise ValueError("Не закрыта скобка")
            self.pos += 1  # пропускаем ')'
            return result

        # Число
        start = self.pos
        while self.pos < len(self.expression) and (
            self.expression[self.pos].isdigit() or self.expression[self.pos] == "."
        ):
            self.pos += 1

        if start == self.pos:
            raise ValueError(
                f"Ожидалось число, получено: '{self.expression[self.pos]}'"
            )

        return float(self.expression[start:self.pos])


def safe_eval(expression: str) -> str:
    """Безопасно вычисляет арифметическое выражение."""
    if not expression or not expression.strip():
        return ""

    try:
        calc = SafeCalculator(expression)
        result = calc.parse()

        # Форматируем результат
        if result == int(result) and abs(result) < 1e15:
            return str(int(result))
        return f"{result:.10g}"

    except ZeroDivisionError:
        return "Ошибка: деление на 0"
    except (ValueError, IndexError) as e:
        return f"Ошибка"


class CalculatorApp:
    """Графический калькулятор на Tkinter."""

    def __init__(self):
        # Создаём главное окно
        self.root = tk.Tk()
        self.root.title("Калькулятор")
        self.root.resizable(False, False)
        self.root.configure(bg="#2b2b2b")

        # Текущее выражение
        self.expression = ""

        # Настраиваем интерфейс
        self._create_display()
        self._create_buttons()

        # Привязка клавиатуры
        self.root.bind("<Key>", self._on_key_press)
        self.root.bind("<Return>", lambda e: self._calculate())
        self.root.bind("<BackSpace>", lambda e: self._backspace())
        self.root.bind("<Escape>", lambda e: self._clear())

    def _create_display(self):
        """Создаёт поле вывода (дисплей калькулятора)."""
        display_frame = tk.Frame(self.root, bg="#2b2b2b", padx=10, pady=10)
        display_frame.pack(fill="x")

        # Строка с выражением (мелкий шрифт, сверху)
        self.expr_label = tk.Label(
            display_frame,
            text="",
            font=("Helvetica", 14),
            bg="#2b2b2b",
            fg="#888888",
            anchor="e",
        )
        self.expr_label.pack(fill="x")

        # Основной дисплей (крупный шрифт)
        self.display_var = tk.StringVar(value="0")
        self.display = tk.Label(
            display_frame,
            textvariable=self.display_var,
            font=("Helvetica", 32, "bold"),
            bg="#2b2b2b",
            fg="#ffffff",
            anchor="e",
        )
        self.display.pack(fill="x")

    def _create_buttons(self):
        """Создаёт сетку кнопок."""
        btn_frame = tk.Frame(self.root, bg="#2b2b2b", padx=5, pady=5)
        btn_frame.pack()

        # Раскладка кнопок: текст, строка, столбец, ширина столбцов, цвет
        buttons = [
            ("C",  0, 0, 1, "#a5a5a5", "#000000"),
            ("()", 0, 1, 1, "#a5a5a5", "#000000"),
            ("%",  0, 2, 1, "#a5a5a5", "#000000"),
            ("/",  0, 3, 1, "#ff9f0a", "#ffffff"),

            ("7",  1, 0, 1, "#505050", "#ffffff"),
            ("8",  1, 1, 1, "#505050", "#ffffff"),
            ("9",  1, 2, 1, "#505050", "#ffffff"),
            ("*",  1, 3, 1, "#ff9f0a", "#ffffff"),

            ("4",  2, 0, 1, "#505050", "#ffffff"),
            ("5",  2, 1, 1, "#505050", "#ffffff"),
            ("6",  2, 2, 1, "#505050", "#ffffff"),
            ("-",  2, 3, 1, "#ff9f0a", "#ffffff"),

            ("1",  3, 0, 1, "#505050", "#ffffff"),
            ("2",  3, 1, 1, "#505050", "#ffffff"),
            ("3",  3, 2, 1, "#505050", "#ffffff"),
            ("+",  3, 3, 1, "#ff9f0a", "#ffffff"),

            ("0",  4, 0, 2, "#505050", "#ffffff"),
            (".",  4, 2, 1, "#505050", "#ffffff"),
            ("=",  4, 3, 1, "#ff9f0a", "#ffffff"),
        ]

        btn_font = tkfont.Font(family="Helvetica", size=18)

        for text, row, col, colspan, bg_color, fg_color in buttons:
            btn = tk.Button(
                btn_frame,
                text=text,
                font=btn_font,
                bg=bg_color,
                fg=fg_color,
                activebackground=bg_color,
                activeforeground=fg_color,
                border=0,
                width=4 * colspan + (colspan - 1),
                height=2,
                command=lambda t=text: self._on_button_click(t),
            )
            btn.grid(row=row, column=col, columnspan=colspan, padx=2, pady=2, sticky="nsew")

        # Настраиваем равномерное распределение столбцов
        for i in range(4):
            btn_frame.grid_columnconfigure(i, weight=1)

    def _on_button_click(self, text: str):
        """Обрабатывает нажатие кнопки."""
        if text == "C":
            self._clear()
        elif text == "=":
            self._calculate()
        elif text == "()":
            self._insert_bracket()
        else:
            self._append(text)

    def _append(self, char: str):
        """Добавляет символ к выражению."""
        if self.expression == "0" and char.isdigit():
            self.expression = char
        else:
            self.expression += char

        self.display_var.set(self.expression)

    def _clear(self):
        """Очищает выражение."""
        self.expression = ""
        self.display_var.set("0")
        self.expr_label.config(text="")

    def _backspace(self):
        """Удаляет последний символ."""
        self.expression = self.expression[:-1]
        self.display_var.set(self.expression if self.expression else "0")

    def _insert_bracket(self):
        """Вставляет открывающую или закрывающую скобку по контексту."""
        open_count = self.expression.count("(")
        close_count = self.expression.count(")")

        # Ставим закрывающую скобку, если есть незакрытые
        # и последний символ — цифра или закрывающая скобка
        if (
            open_count > close_count
            and self.expression
            and (self.expression[-1].isdigit() or self.expression[-1] == ")")
        ):
            self._append(")")
        else:
            self._append("(")

    def _calculate(self):
        """Вычисляет текущее выражение."""
        if not self.expression:
            return

        # Заменяем символы для вычисления
        expr = self.expression.replace("%", "/100*")

        result = safe_eval(expr)

        if result:
            # Показываем выражение сверху
            self.expr_label.config(text=f"{self.expression} =")
            # Показываем результат
            self.expression = result
            self.display_var.set(result)

    def _on_key_press(self, event):
        """Обрабатывает ввод с клавиатуры."""
        char = event.char
        if char in "0123456789.+-*/()":
            self._append(char)

    def run(self):
        """Запускает главный цикл приложения."""
        self.root.mainloop()


if __name__ == "__main__":
    app = CalculatorApp()
    app.run()

Подробный разбор GUI-калькулятора:

Этот пример — самый большой в статье, поэтому разберём его по частям

Класс SafeCalculator — безопасный парсер выражений.* Это рекурсивный нисходящий парсер (recursive descent parser). Он разбирает выражение, учитывая приоритет операций:

  1. Сначала обрабатываются скобки и числа (_parse_primary)
  2. Затем унарный минус (_parse_unary)
  3. Потом умножение и деление (_parse_multiplication)
  4. В последнюю очередь — сложение и вычитание (_parse_addition)

Такой порядок гарантирует, что 2 + 3 * 4 даст 14, а не 20.

Почему не eval()? Функция eval() выполняет произвольный Python-код. Если передать ей __import__('os').system('rm -rf /'), она попытается удалить все файлы на диске. Даже в локальном приложении это плохая привычка — лучше сразу учиться писать безопасный код.

Класс CalculatorApp — графический интерфейс:

  • _create_display — создаёт два текстовых поля: маленькое для выражения и большое для результата.
  • _create_buttons — строит сетку кнопок из списка кортежей. Каждый кортеж содержит текст кнопки, её позицию в сетке, размер и цвета. Параметр command=lambda t=text: self._on_button_click(t) привязывает нажатие кнопки к методу обработки. Обратите внимание на t=text — это фиксирует значение переменной в замыкании (без этого все кнопки вызывали бы обработчик с последним значением text).
  • _insert_bracket — «умная» кнопка скобок. Если есть незакрытые скобки и предыдущий символ — цифра, вставляет ). Иначе — (.
  • Поддержка клавиатуры — метод _on_key_press позволяет набирать выражения не только кнопками, но и с клавиатуры. Enter вычисляет, Backspace удаляет, Escape очищает

Калькулятор как класс (ООП)

Объектно-ориентированное программирование (ООП) — мощная парадигма, которая позволяет организовать код в виде объектов со своими данными и поведением. Создадим калькулятор на Python как класс с историей вычислений, памятью и возможностью расширения

# Калькулятор на Python в стиле ООП
from dataclasses import dataclass, field
from datetime import datetime
import math

@dataclass
class HistoryEntry:
    """Запись в истории вычислений."""
    expression: str
    result: float
    timestamp: datetime = field(default_factory=datetime.now)

    def __str__(self) -> str:
        time_str = self.timestamp.strftime("%H:%M:%S")
        return f"[{time_str}] {self.expression} = {self.result}"


class Calculator:
    """
    Калькулятор с историей вычислений и памятью.
    
    Использование:
        calc = Calculator()
        calc.add(10, 5)       # 15.0
        calc.subtract(10, 3)  # 7.0
        calc.history()        # показать историю
    """

    def __init__(self):
        self._history: list[HistoryEntry] = []
        self._memory: float = 0.0
        self._last_result: float | None = None

    # === Основные операции ===

    def add(self, a: float, b: float) -> float:
        """Сложение."""
        return self._record(f"{a} + {b}", a + b)

    def subtract(self, a: float, b: float) -> float:
        """Вычитание."""
        return self._record(f"{a} - {b}", a - b)

    def multiply(self, a: float, b: float) -> float:
        """Умножение."""
        return self._record(f"{a} * {b}", a * b)

    def divide(self, a: float, b: float) -> float:
        """Деление. Выбрасывает ZeroDivisionError при делении на ноль."""
        if b == 0:
            raise ZeroDivisionError("Деление на ноль невозможно")
        return self._record(f"{a} / {b}", a / b)

    def power(self, base: float, exponent: float) -> float:
        """Возведение в степень."""
        return self._record(f"{base} ** {exponent}", base ** exponent)

    def sqrt(self, value: float) -> float:
        """Квадратный корень."""
        if value < 0:
            raise ValueError("Корень из отрицательного числа не определён")
        return self._record(f"sqrt({value})", math.sqrt(value))

    def modulo(self, a: float, b: float) -> float:
        """Остаток от деления."""
        if b == 0:
            raise ZeroDivisionError("Деление на ноль невозможно")
        return self._record(f"{a} % {b}", a % b)

    # === Память ===

    def memory_store(self, value: float | None = None) -> None:
        """Сохраняет значение в память. Без аргумента — сохраняет последний результат."""
        if value is not None:
            self._memory = value
        elif self._last_result is not None:
            self._memory = self._last_result
        else:
            raise ValueError("Нет значения для сохранения в память")
        print(f"  Сохранено в память: {self._memory}")

    def memory_recall(self) -> float:
        """Возвращает значение из памяти."""
        return self._memory

    def memory_clear(self) -> None:
        """Очищает память."""
        self._memory = 0.0
        print("  Память очищена")

    def memory_add(self, value: float | None = None) -> None:
        """Прибавляет значение к памяти."""
        val = value if value is not None else (self._last_result or 0)
        self._memory += val
        print(f"  Память: {self._memory}")

    # === История ===

    def history(self, count: int = 10) -> list[HistoryEntry]:
        """Возвращает и печатает последние записи истории."""
        entries = self._history[-count:]
        if not entries:
            print("  История пуста")
        else:
            print(f"\n  Последние {len(entries)} вычислений:")
            for entry in entries:
                print(f"    {entry}")
            print()
        return entries

    def clear_history(self) -> None:
        """Очищает историю."""
        self._history.clear()
        print("  История очищена")

    # === Свойства ===

    @property
    def last(self) -> float | None:
        """Последний результат."""
        return self._last_result

    @property
    def memory(self) -> float:
        """Текущее значение в памяти."""
        return self._memory

    # === Внутренние методы ===

    def _record(self, expression: str, result: float) -> float:
        """Записывает операцию в историю и возвращает результат."""
        entry = HistoryEntry(expression=expression, result=result)
        self._history.append(entry)
        self._last_result = result
        return result

    def __repr__(self) -> str:
        return (
            f"Calculator(history={len(self._history)} entries, "
            f"memory={self._memory}, last={self._last_result})"
        )


def interactive_mode():
    """Интерактивный режим калькулятора."""
    calc = Calculator()
    print("=== ООП-калькулятор ===")
    print("Команды: history, mc, mr, ms, m+, clear, q\n")

    while True:
        try:
            raw = input(">>> ").strip()
            if not raw:
                continue

            command = raw.lower()

            # Специальные команды
            if command in ("q", "quit", "выход"):
                print("До свидания!")
                break
            elif command in ("history", "h", "история"):
                calc.history()
                continue
            elif command == "mc":
                calc.memory_clear()
                continue
            elif command == "mr":
                val = calc.memory_recall()
                print(f"  Память: {val}")
                continue
            elif command == "ms":
                calc.memory_store()
                continue
            elif command == "m+":
                calc.memory_add()
                continue
            elif command in ("clear", "очистить"):
                calc.clear_history()
                continue

            parts = raw.split()

            # Унарные операции
            if len(parts) == 2 and parts[0].lower() == "sqrt":
                value = float(parts[1].replace(",", "."))
                result = calc.sqrt(value)
                print(f"  = {result}\n")
                continue

            # Бинарные операции
            if len(parts) == 3:
                a = float(parts[0].replace(",", "."))
                op = parts[1]
                b = float(parts[2].replace(",", "."))

                match op:
                    case "+":
                        result = calc.add(a, b)
                    case "-":
                        result = calc.subtract(a, b)
                    case "*":
                        result = calc.multiply(a, b)
                    case "/":
                        result = calc.divide(a, b)
                    case "**":
                        result = calc.power(a, b)
                    case "%":
                        result = calc.modulo(a, b)
                    case _:
                        print(f"  Неизвестная операция: {op}\n")
                        continue

                print(f"  = {result}\n")
                continue

            print("  Формат: число операция число  или  sqrt число\n")

        except (ValueError, ZeroDivisionError) as e:
            print(f"  Ошибка: {e}\n")
        except KeyboardInterrupt:
            print("\nДо свидания!")
            break


if __name__ == "__main__":
    interactive_mode()

Разбор ООП-калькулятора

  • @dataclass — декоратор, который автоматически создаёт методы __init__, __repr__ и __eq__ на основе аннотаций полей. HistoryEntry хранит выражение, результат и время.
  • field(default_factory=datetime.now) — для изменяемых значений по умолчанию в dataclass используется default_factory. Без этого все записи получили бы одинаковое время.
  • Приватные атрибуты (self._history, self._memory) — в Python одно подчёркивание в начале имени означает «это внутренняя деталь реализации, не трогайте напрямую». Это конвенция, а не жёсткое ограничение.
  • @property — декоратор, превращающий метод в свойство. После этого можно писать calc.last вместо calc.last(). Свойства удобны для чтения данных без возможности их случайно изменить.
  • Метод _record — все операции вызывают его для записи в историю. Это единая точка для логирования, что упрощает добавление новой функциональности (например, экспорта истории в файл).
  • Память калькулятора — имитирует кнопки MS, MR, MC, M+ настоящего калькулятора. memory_store сохраняет значение, memory_recall возвращает, memory_add прибавляет к текущему значению в памяти

Также этот калькулятор можно использовать как библиотеку в другом Python-скрипте:

from calculator_oop import Calculator

calc = Calculator()
total = calc.add(100, 50)      # 150.0
tax = calc.multiply(total, 0.2) # 30.0
print(f"Сумма с налогом: {calc.add(total, tax)}")  # 180.0
calc.history()  # покажет все три операции

Задания для самостоятельной работы

Теория без практики бесполезна. Вот несколько заданий для закрепления материала — от простого к сложному

Задание 1. Калькулятор процентов

Напишите калькулятор, который умеет:

  • Вычислять процент от числа (например, «15% от 200» = 30)
  • Прибавлять процент к числу (например, «200 + 15%» = 230)
  • Вычитать процент из числа (например, «200 — 15%» = 170)

Подсказка: создайте три отдельные функции: percent_of(value, percent), add_percent(value, percent), subtract_percent(value, percent). Формула для процента от числа: value * percent / 100

Задание 2. Калькулятор с конвертацией

Добавьте к любому из примеров возможность конвертации:

  • Километры в мили и обратно (1 км = 0.621371 миль)
  • Цельсий в Фаренгейт и обратно (F = C * 9/5 + 32)
  • Килограммы в фунты и обратно (1 кг = 2.20462 фунтов)

Подсказка: используйте словарь с коэффициентами конвертации. Для температуры понадобятся отдельные функции, так как перевод нелинейный

Задание 3. Калькулятор с сохранением истории в файл

Модифицируйте ООП-калькулятор так, чтобы история вычислений сохранялась в текстовый файл history.txt и загружалась при запуске.

Подсказка: используйте контекстный менеджер with open("history.txt", "a") as f: для дозаписи в файл. При запуске программы прочитайте файл и выведите последние 5 записей

Задание 4. Калькулятор систем счисления

Создайте калькулятор, который работает с числами в разных системах счисления:

  • Двоичная (ввод: 0b1010)
  • Восьмеричная (ввод: 0o12)
  • Шестнадцатеричная (ввод: 0xFF)

Подсказка: функция int("0b1010", 2) преобразует двоичную строку в целое число. Встроенные функции bin(), oct(), hex() выполняют обратное преобразование

Частые ошибки новичков и как их избежать

Разберём типичные ошибки, которые допускают начинающие программисты при создании калькулятора на Python

Ошибка 1: забыли преобразовать ввод в число

# НЕПРАВИЛЬНО — input() возвращает строку!
num1 = input("Число: ")
num2 = input("Число: ")
print(num1 + num2)  # Выведет "105", а не 15 (конкатенация строк)

# ПРАВИЛЬНО
num1 = float(input("Число: "))
num2 = float(input("Число: "))
print(num1 + num2)  # Выведет 15.0

ункция input() всегда возвращает строку. Оператор + для строк — это конкатенация (склеивание), а не сложение. Если ввести 10 и 5, получите "10" + "5" = "105". Всегда оборачивайте input() в int() или float()

Ошибка 2: деление на ноль без проверки

# НЕПРАВИЛЬНО — программа упадёт при вводе 0
result = num1 / num2  # ZeroDivisionError!

# ПРАВИЛЬНО — проверяем перед делением
if num2 != 0:
    result = num1 / num2
else:
    print("Ошибка: деление на ноль!")

Ошибка 3: использование = вместо ==

# НЕПРАВИЛЬНО — это присваивание, а не сравнение
if operation = "+":   # SyntaxError!
    ...

# ПРАВИЛЬНО — двойной знак равенства для сравнения
if operation == "+":
    ...

Одиночный = — оператор присваивания, двойной == — оператор сравнения. В условиях if всегда используется ==

Ошибка 4: неправильные отступы

# НЕПРАВИЛЬНО — IndentationError
if operation == "+":
result = num1 + num2   # нет отступа!

# ПРАВИЛЬНО — 4 пробела отступа
if operation == "+":
    result = num1 + num2

Python использует отступы для обозначения блоков кода. Стандарт — 4 пробела. Не смешивайте пробелы и табуляции — это приведёт к ошибке TabError

Ошибка 5: использование eval() для вычислений

# ОПАСНО — eval() может выполнить ЛЮБОЙ код!
result = eval(input("Выражение: "))
# Если пользователь введёт __import__('os').system('rm -rf /'),
# будут удалены файлы на диске!

# БЕЗОПАСНО — используйте явные операции
# или парсер выражений (как в примере 7 этой статьи)

eval()— одна из самых опасных функций в Python. Она выполняет произвольный код, переданный как строка. Никогда не используйтеeval()` с пользовательским вводом. Для учебных проектов, которые никогда не выйдут в продакшен, это допустимо, но лучше сразу привыкать к безопасным подходам

Ошибка 6: потеря точности при работе с float

# Неожиданный результат:
print(0.1 + 0.2)  # 0.30000000000000004, а не 0.3!

# Решение для финансовых расчётов:
from decimal import Decimal
result = Decimal("0.1") + Decimal("0.2")  # 0.3 (точно!)

# Решение для обычных случаев:
result = round(0.1 + 0.2, 10)  # 0.3

Числа с плавающей точкой (float) хранятся в двоичной системе и не могут точно представить некоторые десятичные дроби. Это не ошибка Python — такое поведение одинаково во всех языках программирования. Для финансовых расчётов используйте модуль decimal


В этой статье мы создали калькулятор на Python девятью разными способами — от простейшего в 15 строк до полноценного GUI-приложения с безопасным парсером выражений. Вот что мы рассмотрели:

  1. Базовый калькулятор — переменные, ввод/вывод, условия if/elif/else
  2. Калькулятор с цикломwhile True, break, continue
  3. Калькулятор с функциями — DRY-принцип, словарь функций, if __name__ == "__main__"
  4. Калькулятор с match/case — современный синтаксис Python 3.10+
  5. Обработка ошибокtry/except, пользовательские исключения, иерархия классов
  6. Продвинутый калькулятор — модуль math, тригонометрия, факториал, константы
  7. GUI-калькулятор — Tkinter, безопасный парсер вместо eval(), обработка событий
  8. ООП-калькулятор — классы, @dataclass, @property, история и память

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

  • Сделать веб-калькулятор на Flask или Django
  • Добавить сохранение истории в SQLite
  • Создать Telegram-бота с функциями калькулятора
  • Написать модульные тесты с помощью pytest

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

Удачи в изучении Python!

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x