Дескрипторы и property

Как перехватить чтение и запись атрибута: дескрипторы вручную и property как готовое решение.

Дескриптор — объект, определяющий методы __get__ и/или __set__, через которые класс управляет доступом к своему атрибуту.

property: управляемый атрибут

Часто нужно, чтобы при обращении к атрибуту что-то вычислялось или проверялось, но снаружи это выглядело как обычное поле. Декоратор property превращает метод в «вычисляемый атрибут» — его читают без скобок.

class Celsius:
    def __init__(self, temp):
        self._temp = temp
    @property
    def fahrenheit(self):
        return self._temp * 9 / 5 + 32

c = Celsius(100)
print("По Фаренгейту:", c.fahrenheit)   # без скобок!
c._temp = 0
print("По Фаренгейту:", c.fahrenheit)

Вывод:

По Фаренгейту: 212.0
По Фаренгейту: 32.0

Снаружи c.fahrenheit выглядит как поле, но на самом деле каждый раз вызывается метод и пересчитывает значение. Это позволяет добавить логику, не меняя интерфейс класса.

Дескриптор с валидацией

property привязан к одному классу. Если одну и ту же логику (например, «значение должно быть неотрицательным») нужно переиспользовать для многих атрибутов и классов, пишут полноценный дескриптор. Метод __set_name__ автоматически узнаёт имя атрибута, __get__ перехватывает чтение, __set__ — запись.

class Positive:
    def __set_name__(self, owner, name):
        self.storage = "_" + name        # куда складывать значение
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.storage)
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("должно быть >= 0")
        setattr(obj, self.storage, value)

class Account:
    balance = Positive()       # дескриптор как атрибут класса
    def __init__(self, balance):
        self.balance = balance # пойдёт через __set__

a = Account(100)
print("Баланс:", a.balance)

try:
    a.balance = -50
except ValueError as e:
    print("Ошибка:", e)

print("Баланс не изменился:", a.balance)

Вывод:

Баланс: 100
Ошибка: должно быть >= 0
Баланс не изменился: 100

Дескриптор Positive можно повесить на любое число атрибутов в любых классах — вся валидация описана один раз. Это и есть преимущество перед property: переиспользование логики доступа.

property — это тоже дескриптор

Под капотом property реализован как дескриптор: у него есть __get__ и __set__. Разница в удобстве: property привязывает геттер/сеттер к конкретному классу, а собственный дескриптор — переиспользуемый объект для многих полей.

КогдаВыбор
один вычисляемый атрибут в одном классеproperty
одна логика доступа для многих атрибутов/классовсвой дескриптор

Итог

  • property делает метод вычисляемым атрибутом — читается без скобок, можно добавить логику без смены интерфейса.
  • Дескриптор (__get__/__set__/__set_name__) перехватывает чтение и запись атрибута.
  • Дескриптор переиспользуем для многих полей; property — для одного атрибута одного класса.
Проверьте себя
1. Как обращаются к атрибуту, помеченному @property?
AСо скобками, как к методу: obj.value()
BБез скобок, как к полю: obj.value
CТолько через getattr
DЧерез индекс obj[value]
2. Какой метод дескриптора перехватывает запись в атрибут?
A__get__
B__set__
C__call__
D__init__
3. В чём главное преимущество собственного дескриптора перед property?
AДескриптор работает быстрее
BОдну логику доступа можно переиспользовать для многих атрибутов и классов
Cproperty нельзя использовать с числами
DДескриптор не требует класса
Поддержать проект