📖 Теория: рекурсия + валидация через исключения

⚡ Кратко

Рекурсия — это вызов функцией самой себя с упрощённой задачей. Обязательно нужен базовый случай, иначе возникнет RecursionError.

Перед рекурсивной обработкой проверяйте тип аргумента через isinstance. При неверных данных сразу выбрасывайте исключение — это называется guard clause.

Внешний вызов оборачивается в try/except, чтобы превратить аварийное завершение в понятное сообщение.

Запомнить: isinstance лучше type(), потому что поддерживает наследование; конкретные исключения лучше общих.

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

Что такое рекурсия

Рекурсивная функция — это функция, которая вызывает саму себя. У каждой такой функции должно быть:

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

Например, сумма списка чисел: базовый случай — пустой список (сумма 0), рекурсивный шаг — первый элемент плюс сумма оставшегося хвоста.

def recursive_sum(numbers: list) -> float:
    """Return the sum of all numbers in a list recursively."""
    if not isinstance(numbers, list):
        raise TypeError("Argument must be a list")
    if not numbers:
        return 0

    head = numbers[0]
    if not isinstance(head, (int, float)):
        raise TypeError(f"All elements must be numbers, got {type(head).__name__}")
    return head + recursive_sum(numbers[1:])


numbers = [1, 2, 3, 4, 5]
try:
    print(recursive_sum(numbers))
except (TypeError, ValueError) as e:
    print(f"Ошибка: {e}")

Каждый рекурсивный вызов добавляет кадр в стек вызовов. Если забыть базовый случай или передать слишком глубокую структуру, Python остановит программу с RecursionError.

Валидация через guard clauses

Guard clause — это проверка в начале функции, которая немедленно прерывает выполнение при нарушении условий. Вместо вложенных if/else мы пишем:

def list_depth(nested_list: list) -> int:
    if not isinstance(nested_list, list):
        raise TypeError("Argument must be a list")
    ...

Такой код проще читать: сначала мы отсекаем всё некорректное, а затем решаем задачу.

isinstance вместо type()

Для проверки типа предпочтительнее isinstance(obj, SomeType), а не type(obj) == SomeType. isinstance учитывает наследование и позволяет проверять несколько типов сразу:

isinstance(42, (int, float))   # True
isinstance([1, 2], list)      # True
isinstance({}, (list, dict))  # True
⚠️ Проверить по документации: использование isinstance вместо type(), guard clauses и type hints выходит за рамки исходного практикума, но является рекомендуемой современной практикой Python.

Обработка исключений при вызове

Функция сообщает о проблеме через raise. Вызывающий код решает, что делать дальше: вывести сообщение, залогировать или прекратить работу. В практикуме каждый вызов оборачивается в try/except, чтобы продемонстрировать корректную обработку.

try:
    print(reverse_string(123))  # не строка
except TypeError as e:
    print(f"Ошибка: {e}")

Краткий итог

Рекурсия + guard clauses + try/except — устойчивая комбинация. Мы сначала проверяем данные, затем решаем задачу рекурсивно, а внешний код корректно реагирует на ошибки.