Сравните два способа декорирования свойства: какой вариант завершится ошибкой при обращении к 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)
Ошибка возникнет при обращении к User().name. Сначала property передаётся в audit, поэтому fn внутри wrapper становится объектом property, а не вызываемой функцией; после связывания wrapper с экземпляром он пытается вызвать этот объект.
Рабочая последовательность — сначала применить audit к функции, затем обернуть результат в property:
Декораторы появились как удобный способ преобразовывать функции или классы во время их определения, не изменяя основной код объекта. Дескрипторы решают другую задачу: они управляют поведением доступа к атрибуту через методы вроде __get__.
property — это дескриптор. Он превращает обращение к атрибуту user.name в вызов функции-геттера. Поэтому порядок преобразований особенно важен: декоратор должен получить функцию, если он рассчитан на вызов функции, а property должен получить готовый геттер.
Запись с несколькими декораторами выполняется снизу вверх. В исходном примере сначала вычисляется property(name), после чего результат передаётся в audit.
audit предполагает, что аргумент fn можно вызвать. Но property предназначен для доступа через атрибут экземпляра и не является обычной функцией-геттером для такого вызова. В результате ошибка проявляется не при создании класса, а только при обращении к User().name, что усложняет диагностику.
Исходная запись эквивалентна такой последовательности:
audit возвращает обычную функцию wrapper. При обращении к User().name эта функция связывается с экземпляром как метод, а затем выполняет fn(*args). Значение fn — объект property, поэтому вызов приводит к TypeError.
Рабочая запись эквивалентна другой последовательности:
Здесь audit сначала создаёт обёртку над исходной функцией. Затем property сохраняет эту обёртку как геттер и при обращении к атрибуту вызывает её с экземпляром.
Общее правило: если один декоратор возвращает дескриптор, а другой ожидает вызываемый объект, сначала применяют декоратор, работающий с функцией, затем дескрипторный декоратор. Это не универсальное правило для любых декораторов, но оно применимо к обычной обёртке функции и property.
У обёртки также желательно сохранять метаданные исходной функции с помощью functools.wraps. Это не исправляет порядок декораторов, но сохраняет имя, документацию и ссылку __wrapped__, что важно для отладки и интроспекции.
В сервисном классе геттер свойства должен регистрировать факт чтения значения. Разработчик написал @audit поверх @property, после чего приложение стало падать только при сериализации объекта: сериализатор обращался к name, а не создавал класс заново.
Можно было исправить проблему несколькими способами. Перенос @audit под @property — самый простой вариант: он сохраняет стандартную семантику свойства и требует минимальных изменений. Можно было также написать специальный декоратор, который умеет оборачивать property, но это увеличило бы сложность и потребовало отдельно поддерживать getter, setter и deleter.
Выбран вариант с @property поверх @audit. Он явно отражает порядок: сначала создаётся вызываемый геттер с аудитом, затем он помещается внутрь дескриптора. В результате чтение свойства корректно логируется, а код класса остаётся обычным для пользователей свойства.
Вопрос: почему ошибка в исходном варианте возникает только при User().name, а не при объявлении класса?
Ответ: тело класса лишь вычисляет и присваивает объекту name результат выражения audit(property(name)). Ни audit, ни property в этом примере не вызывают геттер немедленно. Реальный вызов происходит позже, когда механизм атрибутов экземпляра обнаруживает объект property или, в ошибочном варианте, получает обычный wrapper и запускает его.
Вопрос: что изменится, если audit будет возвращать вызываемый объект без метода __get__?
Ответ: при размещении такого декоратора поверх property проблема останется: результатом станет вызываемый объект, заменивший дескриптор property. Если сам объект не реализует протокол дескриптора, обращение через экземпляр не будет автоматически выполнять логику свойства. Наличие __call__ отвечает за вызов объекта, а наличие __get__ — за его поведение как атрибута класса.
Вопрос: как сохранить возможность назначать значение через setter после добавления аудита к getter?
Ответ: нужно декорировать getter до создания property, а setter объявлять у уже созданного свойства либо также оборачивать соответствующую функцию до передачи её в setter. Например, при использовании синтаксиса @name.setter новый объект свойства сохраняет getter и заменяет setter, поэтому исходный порядок декораторов должен оставаться согласованным с тем, какие функции передаются дескриптору.
Позиция: Python-разработчик Уровень: middle