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

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

Старый: декораторы вручную, кэш на словаре, логирование через print или базовый logging.

Новый: functools.wraps для сохранения метаданных, functools.lru_cache вместо самописного кэша, аннотации типов, f-strings и pathlib.

Вывод: современный код читается лучше и ломается реже.

📜 Как показано в исходном материале: кэш вручную

def cache(func):
    memo = {}

    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        else:
            print("Результат из кеша: ")
        return memo[args]

    return wrapper

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

❌ Почему этот подход может быть не лучшим сейчас

  • Имя и документация декорируемой функции теряются: multiply.__name__ станет 'wrapper'.
  • Кэш неограничен: при большом количестве аргументов растёт потребление памяти.
  • Нет поддержки ключевых аргументов и некортежируемых типов.

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

from functools import lru_cache, wraps


@lru_cache(maxsize=128)
def multiply(a: int, b: int) -> int:
    print(f"Вычисляем {a} * {b}: ")
    return a * b


print(multiply(2, 3))
print(multiply(2, 3))

Что улучшилось: стандартный декоратор реализует LRU-политику, потокобезопасен и сохраняет метаданные функции.

Преимущества:
  • Не нужно писать сложную логику вытеснения.
  • Можно быстро очистить кэш через multiply.cache_clear().
  • Статистика попаданий доступна через multiply.cache_info().

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

import logging

logging.basicConfig(
    filename="call_log.log",
    level=logging.INFO,
    format="%(message)s",
    encoding="utf-8",
)


def log_to_file(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        all_args = ", ".join(str(arg) for arg in args)
        all_kwargs = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
        logging.info(f"function: {func.__name__} | ...")
        return result
    return wrapper

✅ Современный вариант логирования

import logging
from functools import wraps


def log_to_file(func):
    logger = logging.getLogger(func.__module__)

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        logger.info(
            "function: %s | args: %s; kwargs: %s | return: %s",
            func.__name__,
            args or None,
            kwargs or None,
            result,
        )
        return result

    return wrapper

Что улучшилось: используется именованный логгер, @wraps сохраняет метаданные, а формат строки передаётся через %-аргументы (ленивое форматирование, рекомендуемое самим модулем logging).

Проверить по документации: рекомендации по ленивому форматированию в Logging HOWTO.

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

def accepts_types(*expected_types):
    def decorator(func):
        def wrapper(*args):
            for arg, expected in zip(args, expected_types):
                if not isinstance(arg, expected):
                    raise TypeError(f"Incorrect type for argument '{arg}': ...")
            return func(*args)
        return wrapper
    return decorator

Ограничение: обёртка не принимает **kwargs, поэтому ключевые аргументы вызовут ошибку.

✅ Современный вариант проверки типов

from functools import wraps
from inspect import signature


def accepts_types(*expected_types):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            bound = signature(func).bind(*args, **kwargs)
            bound.apply_defaults()
            for param, value in bound.arguments.items():
                if param in expected_by_name and not isinstance(value, expected_by_name[param]):
                    raise TypeError(f"Expected {expected_by_name[param]} for '{param}', got {type(value)}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

Альтернатива: для серьёзной проверки типов используйте аннотации и beartype / pydantic или статический анализатор mypy.

Проверить по документации: аннотации типов — модуль typing, статическая проверка — mypy.

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

Ручное кэширование встречается в legacy-коде и в учебных примерах, где важно понять внутреннее устройство декораторов. В продакшене предпочтительнее functools.lru_cache или специализированные библиотеки кэширования (Redis, memcached). Ручное логирование через print всё ещё встречается в скриптах, но для сервисов используйте модуль logging с именованными логгерами.