⚖️ Старый и современный подход

⚡ Различия в двух словах

Из источника: рекурсивные функции с базовым случаем, уже с type hints.

Современная практика: добавляем docstrings, guard clauses, ранние возвраты, обработку ошибок и заменяем хвостовую рекурсию на итерацию.

Вывод: рекурсия хороша для выразительности, но Python не оптимизирует хвостовые вызовы — не пишите глубокую рекурсию без необходимости.

📜 Как показано в исходном материале

Исходная лекция уже использует аннотации типов и читаемые имена. Функция факториала выглядит так:

# из исходника
def factorial(n: int) -> int:
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)


print(factorial(5))

Этот код корректен и понятен, но не защищён от отрицательных чисел и не объясняет контракт функции.

❌ Что можно улучшить

  • Нет проверки аргументов: factorial(-1) уйдёт в бесконечную рекурсию.
  • Нет docstring — неясно, какие значения допустимы.
  • Хвостовая рекурсия в исходнике показана как концепция, но в Python она не даёт преимуществ.
  • Бинарный поиск в исходнике использует elif/else; современный стиль предпочитает ранние возвраты.

✅ Рекомендуемый современный вариант

# modern_factorial.py
def factorial(n: int) -> int:
    """Возвращает факториал неотрицательного целого числа n."""
    if n < 0:
        raise ValueError("n must be non-negative")
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)


print(factorial(5))   # 120
# print(factorial(-1))  # ValueError

Что улучшилось:

  • Guard clause сразу отсекает некорректные данные.
  • Docstring описывает контракт функции.
  • Ранний возврат уменьшает вложенность и упрощает чтение.

Современный бинарный поиск

# modern_binary_search.py
from typing import Optional


def binary_search(
    arr: list[int],
    target: int,
    left: int,
    right: int
) -> Optional[int]:
    """Возвращает индекс target в отсортированном списке arr или None."""
    if left > right:
        return None

    mid = (left + right) // 2
    if arr[mid] == target:
        return mid
    if arr[mid] < target:
        return binary_search(arr, target, mid + 1, right)
    return binary_search(arr, target, left, mid - 1)

Здесь убраны лишние elif/else: как только функция знает ответ, она сразу возвращает его.

Замена хвостовой рекурсии на итерацию

# modern_iterative_factorial.py
def factorial_iterative(n: int) -> int:
    """Итеративный факториал без риска переполнения стека."""
    if n < 0:
        raise ValueError("n must be non-negative")
    accumulator = 1
    while n > 1:
        accumulator *= n
        n -= 1
    return accumulator

🆕 Современные альтернативы для разделения по типам

⚠️ Проверить по документации: начиная с Python 3.10, для разделения по типам можно использовать конструкцию match-case. В более ранних версиях остаётся isinstance().
# match_dispatch.py
def describe(value) -> str:
    match value:
        case int():
            return f"целое число {value}"
        case str():
            return f"строка длиной {len(value)}"
        case list():
            return f"список из {len(value)} элементов"
        case _:
            return "что-то другое"


print(describe(42))
print(describe("hello"))
print(describe([1, 2, 3]))

Мемоизация рекурсии

⚠️ Проверить по документации: для ускорения рекурсивных функций с повторяющимися вызовами используйте functools.lru_cache. Это не отменяет лимит стека, но сильно сокращает количество вычислений.
# lru_cache_demo.py
from functools import lru_cache


@lru_cache(maxsize=None)
def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(100))

🕰️ Когда старый подход ещё можно встретить

Учебные материалы и простые скрипты часто пишут без guard clauses и docstrings. Это нормально для быстрых экспериментов, но в production-коде и домашних заданиях стоит добавлять проверки и документацию.