Сравните два способа декорирования свойства: какой вариант завершится ошибкой при обращении к User .name и ...

Сравните два способа декорирования свойства: какой вариант завершится ошибкой при обращении к User().name и почему?

def audit(fn):
    def wrapper(*args):
        print("audit")
        return fn(*args)
    return wrapper

class User:
    @audit
    @property
    def name(self):
        return "Ada"

print(User().name)
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Ошибка возникнет при обращении к User().name. Сначала property передаётся в audit, поэтому fn внутри wrapper становится объектом property, а не вызываемой функцией; после связывания wrapper с экземпляром он пытается вызвать этот объект.

Рабочая последовательность — сначала применить audit к функции, затем обернуть результат в property:

def audit(fn): def wrapper(*args): print("audit") return fn(*args) return wrapper class User: @property @audit def name(self): return "Ada" print(User().name) # audit, затем Ada

Исторический контекст

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

property — это дескриптор. Он превращает обращение к атрибуту user.name в вызов функции-геттера. Поэтому порядок преобразований особенно важен: декоратор должен получить функцию, если он рассчитан на вызов функции, а property должен получить готовый геттер.

Постановка проблемы

Запись с несколькими декораторами выполняется снизу вверх. В исходном примере сначала вычисляется property(name), после чего результат передаётся в audit.

audit предполагает, что аргумент fn можно вызвать. Но property предназначен для доступа через атрибут экземпляра и не является обычной функцией-геттером для такого вызова. В результате ошибка проявляется не при создании класса, а только при обращении к User().name, что усложняет диагностику.

Подробное решение

Исходная запись эквивалентна такой последовательности:

name = audit(property(name))

audit возвращает обычную функцию wrapper. При обращении к User().name эта функция связывается с экземпляром как метод, а затем выполняет fn(*args). Значение fn — объект property, поэтому вызов приводит к TypeError.

Рабочая запись эквивалентна другой последовательности:

name = property(audit(name))

Здесь audit сначала создаёт обёртку над исходной функцией. Затем property сохраняет эту обёртку как геттер и при обращении к атрибуту вызывает её с экземпляром.

Общее правило: если один декоратор возвращает дескриптор, а другой ожидает вызываемый объект, сначала применяют декоратор, работающий с функцией, затем дескрипторный декоратор. Это не универсальное правило для любых декораторов, но оно применимо к обычной обёртке функции и property.

У обёртки также желательно сохранять метаданные исходной функции с помощью functools.wraps. Это не исправляет порядок декораторов, но сохраняет имя, документацию и ссылку __wrapped__, что важно для отладки и интроспекции.

Ситуация из практики

В сервисном классе геттер свойства должен регистрировать факт чтения значения. Разработчик написал @audit поверх @property, после чего приложение стало падать только при сериализации объекта: сериализатор обращался к name, а не создавал класс заново.

Можно было исправить проблему несколькими способами. Перенос @audit под @property — самый простой вариант: он сохраняет стандартную семантику свойства и требует минимальных изменений. Можно было также написать специальный декоратор, который умеет оборачивать property, но это увеличило бы сложность и потребовало отдельно поддерживать getter, setter и deleter.

Выбран вариант с @property поверх @audit. Он явно отражает порядок: сначала создаётся вызываемый геттер с аудитом, затем он помещается внутрь дескриптора. В результате чтение свойства корректно логируется, а код класса остаётся обычным для пользователей свойства.

Что кандидаты часто упускают

  1. Вопрос: почему ошибка в исходном варианте возникает только при User().name, а не при объявлении класса?

    Ответ: тело класса лишь вычисляет и присваивает объекту name результат выражения audit(property(name)). Ни audit, ни property в этом примере не вызывают геттер немедленно. Реальный вызов происходит позже, когда механизм атрибутов экземпляра обнаруживает объект property или, в ошибочном варианте, получает обычный wrapper и запускает его.

  2. Вопрос: что изменится, если audit будет возвращать вызываемый объект без метода __get__?

    Ответ: при размещении такого декоратора поверх property проблема останется: результатом станет вызываемый объект, заменивший дескриптор property. Если сам объект не реализует протокол дескриптора, обращение через экземпляр не будет автоматически выполнять логику свойства. Наличие __call__ отвечает за вызов объекта, а наличие __get__ — за его поведение как атрибута класса.

  3. Вопрос: как сохранить возможность назначать значение через setter после добавления аудита к getter?

    Ответ: нужно декорировать getter до создания property, а setter объявлять у уже созданного свойства либо также оборачивать соответствующую функцию до передачи её в setter. Например, при использовании синтаксиса @name.setter новый объект свойства сохраняет getter и заменяет setter, поэтому исходный порядок декораторов должен оставаться согласованным с тем, какие функции передаются дескриптору.

Позиция: Python-разработчик Уровень: middle