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

Разбираем декораторы с аргументами, классы-декораторы и помощники из 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Чтобы разрешить декоратору аргументы
CЧтобы кешировать результаты
DЧтобы обёртка сохранила имя и docstring исходной функции
2. Сколько уровней вложенных функций нужно декоратору, который принимает собственные аргументы?
AТри
BОдин
CДва
DАргументы декораторам недоступны
3. Что делает functools.lru_cache?
AЗапускает функцию в отдельном потоке
BКеширует результаты по аргументам, чтобы не вычислять повторно
CЛогирует все вызовы функции
DОграничивает время выполнения функции
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →