Функции в Python — объекты: их можно передавать и возвращать. Декоратор этим пользуется: получает функцию, определяет внутри обёртку, которая что-то делает до и после вызова, и возвращает обёртку.
Запись
@timer
def work(): ...
полностью эквивалентна work = timer(work) и выполняется один раз — при определении функции.
Обёртка — замыкание: она помнит исходную функцию из внешней области. Чтобы подходить к любой функции, обёртка принимает *args, **kwargs и передаёт их дальше, а результат возвращает.
Без @functools.wraps(func) у обёртки будет имя wrapper и пустой docstring — это ломает логи, отладку и документацию.
Декоратор с параметрами (@retry(3)) — это функция, которая возвращает декоратор: снаружи параметры, внутри декоратор, ещё глубже обёртка.
Примеры из жизни: @property, @staticmethod, @functools.lru_cache, маршруты во Flask и FastAPI (@app.get("/")), проверка прав, логирование, повторные попытки.