Классы-декораторы в Python

Декораторы-классы в Python: __call__, передача аргументов через __init__, functools.wraps и сравнение с функциями-декораторами.

Декоратор-класс — это класс, экземпляр которого является вызываемым (callable). Чтобы экземпляр вёл себя как функция, достаточно определить метод __call__. Аргументы декоратора передаются в __init__, а декорируемая функция — в __call__.

Почему именно класс, а не функция

Функция-декоратор хороша для простых случаев. Но когда нужно хранить состояние между вызовами, вести счётчик или поддерживать конфигурацию — класс удобнее: данные живут в атрибутах self.

Простейший декоратор-класс

Начнём с декоратора без аргументов — он оборачивает функцию и выводит «звёздочки»:

class Stars:
    def __init__(self, fn):
        self.fn = fn        # сохраняем функцию как атрибут

    def __call__(self, *args, **kwargs):
        print("*" * 10)
        result = self.fn(*args, **kwargs)
        print(result)
        print("*" * 10)
        return result


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


greet("Алиса")

Вывод:

**********
Привет, Алиса!
**********

При применении @Stars Python вызывает Stars(greet) — то есть __init__ получает декорируемую функцию напрямую.

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

Если декоратор принимает аргументы (@Stars(5)), схема немного другая: __init__ получает аргументы, а __call__ — декорируемую функцию и должен вернуть обёртку:

from functools import wraps


class Stars:
    def __init__(self, n):
        self.n = n          # аргумент декоратора

    def __call__(self, fn):
        @wraps(fn)          # сохраняем __name__, __doc__ оригинала
        def wrapper(*args, **kwargs):
            print(self.n * "*")
            result = fn(*args, **kwargs)
            print(result)
            print(self.n * "*")
            return result
        return wrapper


@Stars(5)
def add(a, b):
    return a + b


add(10, 20)

Вывод:

*****
30
*****

Конструкция @Stars(5) эквивалентна:

add = Stars(5)(add)
  1. Stars(5) → создаёт экземпляр, self.n = 5.
  2. instance(add) → вызывает __call__(add) → возвращает wrapper.

Декоратор со счётчиком вызовов

Здесь классовый декоратор особенно хорош — состояние хранится в атрибуте:

from functools import wraps


class CallCounter:
    """Считает, сколько раз вызвана функция."""

    def __init__(self, fn):
        wraps(fn)(self)      # копируем метаданные функции на экземпляр
        self.fn = fn
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.fn(*args, **kwargs)


@CallCounter
def say(message):
    print(message)


say("раз")
say("два")
say("три")
print(f"Вызвано {say.count} раз(а)")

Вывод:

раз
два
три
Вызвано 3 раз(а)

Функция-декоратор vs класс-декоратор

КритерийФункция-декораторКласс-декоратор
Синтаксисзамыкание + вложенные defкласс + __init__ + __call__
Хранение состояниячерез замыканиечерез атрибуты self — явнее
Наследование/расширениенеудобнолегко — подклассы
Читаемостькоротко для простых случаевпонятнее при сложной логике

functools.wraps — зачем он нужен

from functools import wraps


class Log:
    def __init__(self, fn):
        self.fn = fn         # БЕЗ wraps

    def __call__(self, *args, **kwargs):
        print(f"Вызов {self.fn.__name__}")
        return self.fn(*args, **kwargs)


@Log
def compute(x):
    """Вычисляет квадрат."""
    return x ** 2


print(compute.__name__)   # → compute (берётся из self.fn)
print(compute.__doc__)    # → None! wraps не применён

Вывод:

compute
None

Применяйте @wraps(fn) к обёртке или wraps(fn)(self) в __init__, чтобы сохранить __name__, __doc__ и __module__ оригинальной функции. Это важно для документации, help() и интроспекции.

Коротко

  • Класс становится декоратором благодаря методу __call__.
  • Декоратор без аргументов: __init__ получает функцию, __call__ — вызывает её.
  • Декоратор с аргументами: __init__ получает аргументы, __call__ — функцию, возвращает обёртку.
  • Используйте functools.wraps, чтобы сохранить метаданные оригинала.
  • Классы удобнее функций, когда декоратор хранит состояние (счётчики, кэши, конфиги).
Проверьте себя
1. Что делает метод __call__ в классе Star из примера?
AИнициализирует атрибут n при создании экземпляра
BВызывается при удалении объекта
CОпределяет строковое представление объекта
DПозволяет использовать экземпляр класса как callable-декоратор
2. Что происходит при использовании @Star(5) перед функцией add?
AВызывается только __init__ с аргументом add
BPython ищет функцию с именем Star5 в глобальном пространстве имён
Cadd декорируется с помощью стандартного механизма @
DСоздаётся экземпляр Star(5) и сразу вызывается __call__(add)
3. Где хранится аргумент n при использовании класса Star как декоратора?
AВ глобальной переменной
BВ атрибуте self.n экземпляра класса Star
CПередаётся в каждый вызов wrapper заново
DВ замыкании функции wrapper

Закрепите практикой

Задачи с автоматической проверкой — решайте прямо здесь, не уходя из учебника.

py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →