Декораторы вглубь

Разбираем декораторы с аргументами, классы-декораторы и помощники из functools.

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

Декоратор с аргументами

Обычный декоратор — функция от функции. Чтобы декоратор принимал свои параметры (например, «повтори N раз»), нужен ещё один уровень: внешняя функция принимает аргумент и возвращает сам декоратор.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Привет, {name}!")
    return name

greet("Аня")
print("Имя функции:", greet.__name__)

Вывод:

Привет, Аня!
Привет, Аня!
Привет, Аня!
Имя функции: greet

Зачем functools.wraps

Без @functools.wraps(func) обёртка «съедает» метаданные исходной функции: greet.__name__ стал бы "wrapper", а docstring исчез бы. wraps копирует имя, документацию и другие атрибуты с оригинала на обёртку — поэтому в выводе мы видим greet, а не wrapper. Всегда используйте его в декораторах.

Класс-декоратор

Декоратором может быть и класс — если у него есть метод __call__. Это удобно, когда декоратору нужно хранить состояние между вызовами, например счётчик.

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Вызов №{self.count} функции {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hi():
    print("Привет!")

say_hi()
say_hi()
print("Всего вызовов:", say_hi.count)

Вывод:

Вызов №1 функции say_hi
Привет!
Вызов №2 функции say_hi
Привет!
Всего вызовов: 2

Состояние (self.count) живёт в экземпляре класса — после декорирования say_hi это объект CountCalls, поэтому у него есть атрибут count.

lru_cache: мемоизация из коробки

functools.lru_cache — готовый декоратор, кеширующий результаты функции по её аргументам. Повторный вызов с теми же аргументами возвращает сохранённое значение мгновенно, без пересчёта.

import functools

calls = {"n": 0}

@functools.lru_cache(maxsize=None)
def slow_square(x):
    calls["n"] += 1      # считаем реальные вычисления
    return x * x

print(slow_square(4))
print(slow_square(4))    # из кеша, без вычисления
print(slow_square(5))
print("Реальных вычислений:", calls["n"])
print(slow_square.cache_info())

Вывод:

16
16
25
Реальных вычислений: 2
CacheInfo(hits=1, misses=2, maxsize=None, currsize=2)

Заметьте: slow_square(4) вызван дважды, но реальных вычислений всего два (на 4 и на 5) — второй запрос на 4 взят из кеша. Это незаменимо для дорогих чистых функций, например рекурсивных (числа Фибоначчи).

Итог

  • Декоратор с аргументами — это три уровня функций: внешняя берёт параметры и возвращает декоратор.
  • functools.wraps сохраняет имя и docstring исходной функции на обёртке.
  • Класс-декоратор (через __call__) удобен для хранения состояния между вызовами.
  • lru_cache кеширует результаты по аргументам — мгновенная мемоизация.
Проверьте себя
1. Зачем в декораторе используют functools.wraps?
AЧтобы декоратор работал быстрее
BЧтобы обёртка сохранила имя и docstring исходной функции
CЧтобы разрешить декоратору аргументы
DЧтобы кешировать результаты
2. Сколько уровней вложенных функций нужно декоратору, который принимает собственные аргументы?
AОдин
BДва
CТри
DАргументы декораторам недоступны
3. Что делает functools.lru_cache?
AЗапускает функцию в отдельном потоке
BКеширует результаты по аргументам, чтобы не вычислять повторно
CЛогирует все вызовы функции
DОграничивает время выполнения функции
Поддержать проект