Как добавить кастомный property в класс без изменения кода
Что проверяют
Понимаете ли вы, что property это дескриптор и работает только как атрибут класса, а не экземпляра. Следом спрашивают, чем такой манкипатчинг опасен и какие есть альтернативы.
Как отвечать
Классы в Python изменяемы, поэтому свойство можно навесить снаружи:
def full_name(self):
return f"{self.first} {self.last}"
User.full_name = property(full_name)
# или setattr(User, "full_name", property(full_name))Это работает, потому что при обращении obj.full_name Python ищет атрибут в классе, находит дескриптор с __get__ и вызывает его. Изменение сразу видят все существующие и будущие экземпляры. Для записи передайте второй аргумент: property(fget, fset).
Что ещё стоит назвать:
- наследник или миксин: самый честный способ, если вы контролируете место создания объектов;
- декоратор класса: функция принимает класс, добавляет свойства и возвращает его;
__init_subclass__или метакласс: когда свойства нужно добавлять всем наследникам по правилу;__getattr__: вызывается только для отсутствующих атрибутов, подходит для вычисляемых полей по шаблону;functools.cached_property: если присваиваете его после создания класса, вызовите__set_name__(cls, name)вручную, иначе первое обращение упадёт сTypeError.
Отдельно скажите о цене. Манкипатчинг чужого класса меняет поведение во всём процессе, включая сторонние библиотеки, mypy и IDE нового атрибута не видят, а после обновления библиотеки патч может молча перестать работать. В тестах для временной подмены есть unittest.mock.patch.object, он сам вернёт исходное состояние.
Где спотыкаются
Присваивают свойство экземпляру: obj.full_name = property(...). Объект property просто ляжет в obj.__dict__ и вернётся как есть, дескрипторы на экземплярах не срабатывают.
Пробуют то же со встроенными типами. int.double = property(...) падает с TypeError: встроенные типы, написанные на C, менять нельзя.
Путают с __slots__. Слоты запрещают новые атрибуты экземпляра, но не класса, поэтому property добавить можно. А вот cached_property на классе без __dict__ работать не будет, ему негде сохранить значение.