Skip to content

Latest commit

 

History

History
566 lines (374 loc) · 44.3 KB

File metadata and controls

566 lines (374 loc) · 44.3 KB

Урок 9. Дескрипторы в Python: контроль доступа к атрибутам на уровне языка

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

Рассмотрим класс точки в трёхмерном пространстве, где каждая координата обязана быть целым числом:

class Point3D:
    @classmethod
    def verify_coord(cls, coord):
        if type(coord) != int:
            raise TypeError("Координата должна быть целым числом")

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, coord):
        self.verify_coord(coord)
        self._x = coord

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, coord):
        self.verify_coord(coord)
        self._y = coord

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, coord):
        self.verify_coord(coord)
        self._z = coord

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

Проверка verify_coord действительно вынесена в отдельный метод и не дублируется по содержанию. Но сама структура — пара @property/@setter, обращение к своему собственному приватному атрибуту, вызов проверки — повторена три раза почти дословно, отличаясь только именем атрибута (x, y, z вместо _x, _y, _z). Если бы полей было не три, а пятнадцать или двадцать, объём практически идентичного, механически копируемого кода стал бы серьёзной проблемой сопровождения: любое изменение в логике проверки пришлось бы вручную вносить в каждое из пятнадцати мест.

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

Что такое дескриптор

Дескриптор — это класс, который управляет доступом к атрибуту другого класса, реализуя один или несколько специальных методов: __get__ — для чтения, __set__ — для записи, __delete__ — для удаления. Если объект такого класса присваивается в качестве атрибута класса, Python начинает обращаться к нему не напрямую, а через эти методы всякий раз, когда происходит обращение к соответствующему имени у экземпляра.

Различают два вида дескрипторов. Дескриптор без данных (non-data descriptor) реализует только __get__ — он способен перехватывать чтение, но не запись. Дескриптор с данными (data descriptor) реализует и __get__, и __set__ (а возможно, и __delete__) — он способен полностью контролировать все обращения к атрибуту. Разница между ними не декоративная: как будет показано дальше в этом уроке, от неё напрямую зависит приоритет дескриптора по отношению к обычным атрибутам объекта.

Первая реализация дескриптора

Создадим дескриптор, отвечающий за хранение целого числа:

class Integer:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

Применим его к классу Point3D:

class Point3D:
    x = Integer()
    y = Integer()
    z = Integer()

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
pt = Point3D(1, 2, 3)
print(pt.__dict__)   # {'_x': 1, '_y': 2, '_z': 3}

Обратите внимание на итоговую структуру класса: теперь одна и та же логика (класс Integer) обслуживает сразу три координаты, и добавление четвёртой координаты потребовало бы одной строки (w = Integer()), а не копирования блока из шести строк, как это было бы с property. Чтобы понять, как это работает изнутри, разберём происходящее по шагам.

Что происходит при определении класса

Когда Python выполняет тело класса Point3D и доходит до строки x = Integer(), происходит следующее: сначала создаётся объект класса Integer — обычный вызов конструктора, ничем не отличающийся от создания любого другого объекта. Этот объект сохраняется в пространстве имён класса Point3D под именем x, то есть Point3D.__dict__['x'] теперь содержит этот объект Integer. И сразу вслед за этим Python вызывает специальный метод __set_name__ у только что созданного объекта, автоматически передавая ему класс, в котором дескриптор был объявлен (owner), и то имя, под которым он был сохранён (name).

def __set_name__(self, owner, name):
    self.name = "_" + name

Здесь owner равен Point3D, а name равен 'x'. Метод формирует и запоминает имя, под которым фактическое значение будет храниться внутри конкретного объекта: '_x'. Точно такой же вызов происходит для y и z, каждый раз с соответствующим именем. __set_name__ вызывается Python-ом автоматически, единственный раз — в момент создания класса, а не в момент создания какого-либо объекта, — и это тот механизм, который избавляет от необходимости писать self.name = "_x" вручную для каждого дескриптора отдельно: имя вычисляется автоматически, исходя из того, под каким именем сам дескриптор был объявлен в классе.

Что происходит при создании объекта

Выполним pt = Point3D(1, 2, 3). Внутри __init__ выполняется строка self.x = x. На первый взгляд это обычное присваивание через объект, разобранное ещё во втором уроке, — но на этот раз оно ведёт себя иначе. Python обнаруживает, что x в классе Point3D — это дескриптор с данными (у Integer определён __set__), и вместо обычного создания записи x в pt.__dict__ вызывает:

Integer.__set__(дескриптор_x, instance=pt, value=1)

Параметры этого метода стоит разобрать явно: self — это сам объект дескриптора (тот, что хранится в Point3D.__dict__['x']), instance — это объект, с которым сейчас работает код (pt), а value — присваиваемое значение (1). Тело метода выполняет:

instance.__dict__[self.name] = value

то есть фактически pt.__dict__['_x'] = 1. Обратите внимание: значение сохраняется не под именем x, а под именем _x, вычисленным ранее в __set_name__. Именно поэтому pt.__dict__, если на него взглянуть напрямую, не содержит ключа x вовсе — там присутствуют только _x, _y, _z.

Что происходит при чтении значения

Выполним print(pt.x). Python снова обнаруживает, что x — дескриптор, и на этот раз вызывает:

Integer.__get__(дескриптор_x, instance=pt, owner=Point3D)

instance — это pt, owner — класс Point3D. Здесь стоит обратить внимание на не самый очевидный случай: если обратиться к дескриптору не через объект, а напрямую через класс — Point3D.x — то instance окажется равен None, а owner по-прежнему будет содержать Point3D. Это позволяет дескриптору вести себя по-разному в зависимости от того, вызван ли он для конкретного объекта или для класса в целом, хотя в приведённой реализации Integer эта возможность не используется.

Тело __get__ выполняет:

return instance.__dict__[self.name]

то есть возвращает pt.__dict__['_x'], а значит, 1.

Всю эту последовательность удобно представить в виде короткой схемы:

Запись:  pt.x = 10   →  Integer.__set__(pt, 10)  →  pt.__dict__['_x'] = 10
Чтение:  pt.x        →  Integer.__get__(pt, Point3D)  →  return pt.__dict__['_x']

Добавление валидации

Ровно то же место, где property выполнял бы проверку в своём сеттере, дескриптор использует в собственном __set__:

class Integer:
    @classmethod
    def verify_coord(cls, value):
        if type(value) != int:
            raise TypeError("Координата должна быть целым числом")

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        self.verify_coord(value)
        setattr(instance, self.name, value)
pt = Point3D(1, 2, 3)
pt.x = 10       # проходит проверку
pt.y = "test"   # TypeError: Координата должна быть целым числом

Здесь стоит объяснить замену instance.__dict__[self.name] на getattr(instance, self.name) и setattr(instance, self.name, value). Обе формы записи в данном конкретном случае дают одинаковый результат, поскольку self.name (например, '_x') — это обычное имя, не связанное ни с каким другим дескриптором, а значит, getattr/setattr в итоге всё равно обращаются к __dict__ объекта напрямую, никакого дополнительного дескриптора по пути не встречая. getattr/setattr считаются более аккуратной формой записи, потому что они уважают весь обычный протокол поиска и присваивания атрибутов, включая случаи, не предусмотренные этой простой реализацией (например, если бы у класса Point3D был собственный __setattr__, что будет разбираться в одном из следующих уроков). Прямое обращение к instance.__dict__[...], напротив, полностью обходит любую другую логику, которая могла бы быть связана с обычным присваиванием атрибута, и потому в более сложных сценариях может привести к неожиданным результатам.

Здесь же стоит отметить важную ловушку, которой в этом коде удаётся избежать только благодаря __set_name__: если бы self.name хранил не '_x', а 'x' — то есть точно такое же имя, каким назван сам дескриптор в классе, — вызов setattr(instance, 'x', value) внутри __set__ заново обратился бы к тому же самому дескриптору x, что вызвало бы повторный вызов __set__, который снова обратился бы к setattr(instance, 'x', ...), и так далее до бесконечности, пока программа не завершится ошибкой переполнения стека вызовов. Именно поэтому __set_name__ вычисляет для реального хранения значение имя, отличное от имени самого дескриптора (_x вместо x), — без этого различия дескриптор был бы попросту неработоспособен.

Почему приоритет дескриптора зависит от наличия __set__

Создадим дескриптор, реализующий только __get__, — non-data descriptor:

class ReadIntX:
    def __set_name__(self, owner, name):
        self.name = "_x"

    def __get__(self, instance, owner):
        return getattr(instance, self.name)


class Point3D:
    x = Integer()
    y = Integer()
    z = Integer()

    xr = ReadIntX()

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
pt = Point3D(1, 2, 3)
print(pt.xr)   # 1 — чтение работает, __get__ вызывается

pt.xr = 100
print(pt.xr)   # 100

Второе присваивание не вызвало никакой ошибки, но результат неожиданный: значение изменилось, хотя у ReadIntX вообще нет метода __set__. Разгадка в том, что произошло на самом деле: раз у дескриптора ReadIntX нет __set__, Python не имеет способа перехватить операцию записи через него, и присваивание pt.xr = 100 выполняется как совершенно обычное присваивание через объект — создаётся (точнее, теперь уже существует) отдельная запись xr непосредственно в pt.__dict__. С этого момента при чтении pt.xr Python сначала находит xr прямо в словаре объекта — согласно правилу поиска атрибутов, разобранному ещё во втором уроке, — и это обычное значение из __dict__ объекта перекрывает дескриптор класса, поскольку non-data descriptor не обладает приоритетом над содержимым __dict__ экземпляра.

Теперь добавим __set__ в тот же дескриптор — превратив его в data descriptor:

class ReadIntX:
    def __set_name__(self, owner, name):
        self.name = "_x"

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        setattr(instance, self.name, value)
pt = Point3D(1, 2, 3)
pt.__dict__['xr'] = 100
print(pt.xr)   # 1

На этот раз даже прямое, «в обход» изменение словаря объекта (pt.__dict__['xr'] = 100) не повлияло на то, что вернёт pt.xr. Причина в порядке поиска, которым руководствуется Python: если в классе объявлен data descriptor (обладающий и __get__, и __set__), он проверяется прежде, чем содержимое __dict__ самого объекта, и потому обращение pt.xr вызывает ReadIntX.__get__, полностью игнорируя то, что лежит в pt.__dict__['xr']. Non-data descriptor, напротив, проверяется только тогда, когда в __dict__ объекта не нашлось одноимённой записи — то есть уступает объекту приоритет.

Этот же самый принцип, отметим, объясняет и поведение property, разобранное в прошлом уроке: property, реализующий __get__, __set__ и __delete__, — это в точности data descriptor, и именно поэтому он всегда обладает приоритетом над содержимым __dict__ объекта, даже если туда что-то записать напрямую в обход свойства.

Дескрипторы и инкапсуляция

До этого момента мы говорили о дескрипторе как о механизме, который перехватывает чтение и запись атрибута. Теперь важно связать это с понятием инкапсуляции.

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

В нашем примере:

pt.x = 10

не приводит к обычной записи x в pt.__dict__. Вместо этого вызывается Integer.__set__(), который проверяет значение и сохраняет его под другим именем:

pt.__dict__['_x'] = 10

При чтении происходит обратная операция:

pt.x

вызывает Integer.__get__(), который получает значение из внутреннего хранилища _x.

Таким образом, внешний код не обязан знать, где и каким образом фактически хранится значение. Он работает с x, а дескриптор управляет тем, что происходит внутри. В этом и заключается инкапсуляция логики доступа.

При этом _x — это не специальный механизм Python для сокрытия данных. Одно ведущее подчёркивание является соглашением: оно сообщает программистам, что атрибут считается внутренним и не предназначен для обычного использования извне.

Отдельно стоит вспомнить имя с двумя ведущими подчёркиваниями:

class Point:
    def __init__(self, x):
        self.__x = x

Здесь Python применяет name mangling и фактически сохраняет значение под именем, содержащим имя класса:

point.__dict__
# {'_Point__x': 10}

Однако важно понимать, что name mangling не является частью механизма дескрипторов. Если имя формируется динамически и передаётся в getattr() или setattr() как строка, Python не выполняет это преобразование автоматически:

setattr(instance, "__x", 10)

В этом случае в __dict__ появится именно ключ __x, а не _Point__x:

instance.__dict__
# {'__x': 10}

Поэтому дескриптор, если ему требуется использовать имя по аналогии с __x, должен сформировать его самостоятельно. Например:

class PrivateInteger:
    def __set_name__(self, owner, name):
        self.name = f"_{owner.__name__}__{name}"

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        if type(value) != int:
            raise TypeError("Значение должно быть целым числом")

        setattr(instance, self.name, value)

Теперь можно использовать этот дескриптор как обычный атрибут:

class Point3D:
    x = PrivateInteger()

    def __init__(self, x):
        self.x = x


pt = Point3D(10)

print(pt.__dict__)
# {'_Point3D__x': 10}

Здесь важно различать два механизма. В обычной записи self.__x имя преобразовывается Python автоматически. В нашем дескрипторе строка "_Point3D__x" сформирована вручную внутри __set_name__.

На практике для дескриптора обычно достаточно обычного _x, как в классе Integer. Нам важно лишь отделить имя, через которое работает внешний код (x), от имени внутреннего хранения (_x). Если же дескриптор используется в более сложной библиотеке и необходимо избежать конфликтов имён между классами или наследниками, он может самостоятельно формировать более специфичное внутреннее имя.

Таким образом, дескриптор предоставляет механизм контроля доступа, а внутреннее имя хранения и name mangling решают отдельную задачу именования. Их можно использовать вместе, но это разные механизмы Python.

Дескрипторы — это то, на чём построен property

Стоит явно проговорить связь, которая до этого момента оставалась неявной: декоратор @property — это, по сути, удобный способ создать data descriptor, не объявляя для этого отдельный класс вручную. Когда вы пишете:

class Person:
    @property
    def old(self):
        return self.__old

    @old.setter
    def old(self, value):
        self.__old = value

интерпретатор за кулисами создаёт объект встроенного класса property, у которого реализованы и __get__, и __set__, и который внутри себя хранит ссылки на переданные ему функции-геттер и функции-сеттер, вызывая нужную из них при каждом обращении. Иначе говоря, property — это готовый, встроенный в язык дескриптор общего назначения, рассчитанный на работу с одним конкретным атрибутом одного конкретного класса. Дескрипторы, написанные вручную, — тот же самый механизм, но выведенный на уровень более общего инструмента, который можно параметризовать (как в примере с Integer, одинаково пригодным для x, y и z) и переиспользовать между разными классами.

Почему дескрипторы редко пишут вручную

Несмотря на всю мощь этого механизма, в повседневном коде прикладных программ дескрипторы пишут вручную нечасто — гораздо чаще для тех же задач используют property или обычные атрибуты. Причина в балансе выгоды и сложности: дескриптор требует отдельного класса, понимания жизненного цикла __set_name__/__get__/__set__, понимания разницы между data и non-data дескрипторами и того, как эта разница сказывается на приоритете доступа. Всё это заметно сложнее для чтения и отладки, чем обычный property, и оправдывает себя только тогда, когда одну и ту же логику контроля доступа действительно предстоит применить к достаточно большому числу атрибутов или классов, чтобы избавление от дублирования перевесило возросшую сложность.

Где дескрипторы реально применяются

Практическое применение дескрипторов в основном сосредоточено в библиотеках и фреймворках, а не в прикладном коде конечных приложений — и понимание этого объясняет, почему сам механизм тем не менее исключительно важен, даже если вы никогда не напишете собственный дескриптор в обычном учебном или рабочем проекте.

Самый заметный пример — ORM-библиотеки, такие как Django ORM или SQLAlchemy, где поля модели объявляются подобным образом:

class User(Model):
    name = StringField()
    age = IntegerField()

Здесь StringField и IntegerField — это дескрипторы, каждый из которых валидирует присваиваемое значение, отвечает за его хранение и, в конечном счёте, за отображение этого поля на столбец таблицы в базе данных. Одна и та же логика поля переиспользуется для сотен различных моделей во всём проекте — ровно тот сценарий, где выгода от дескриптора многократно окупает его сложность.

Второй пример — переиспользуемая валидация в собственном прикладном коде, когда сразу несколько полей нуждаются в одинаковой проверке:

class Product:
    price = PositiveNumber()
    weight = PositiveNumber()
    quantity = PositiveNumber()

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

Третий и четвёртый примеры — ленивая загрузка (lazy loading) и кеширование, где __get__ вычисляет значение только при первом обращении, а затем сохраняет его, чтобы не повторять дорогостоящее вычисление при последующих обращениях:

class CachedValue:
    def __get__(self, instance, owner):
        if self.name not in instance.__dict__:
            instance.__dict__[self.name] = compute()
        return instance.__dict__[self.name]

Такой дескриптор особенно полезен, если compute() представляет собой, например, тяжёлый сетевой запрос или ресурсоёмкое вычисление, которое нежелательно повторять при каждом обращении к атрибуту.

Наконец, дескрипторы применяют для логирования доступа к атрибутам (когда нужно отследить каждое чтение или изменение конкретного поля в целях отладки) и для контроля доступа по правам (когда чтение атрибута разрешено только при выполнении определённого условия, например наличия соответствующих прав у текущего пользователя). Все эти случаи объединяет одно: логика, не зависящая от конкретного класса по своей сути, но применяемая к атрибутам многих разных классов.

Подводные камни

Главная ловушка, уже разобранная выше, — присвоить внутреннему имени хранения то же значение, что и имени самого дескриптора, из-за чего __get__/__set__ начинают бесконечно вызывать сами себя через getattr/setattr. Именно поэтому __set_name__ всегда должен формировать имя, отличное от имени, под которым дескриптор виден снаружи (_x вместо x, а не x вместо x).

Вторая ловушка — забыть о разнице между data и non-data дескрипторами и удивиться, когда non-data descriptor «перестаёт работать» после присваивания. Как было показано, non-data descriptor уступает приоритет любой одноимённой записи, оказавшейся в __dict__ объекта, — и если требуется, чтобы дескриптор действительно контролировал запись, а не только чтение, необходимо реализовать __set__, даже если внутри него нет никакой содержательной логики, кроме сохранения значения.

Третья ловушка — путать место объявления дескриптора. Дескриптор всегда объявляется как атрибут класса (x = Integer() в теле Point3D), а не как атрибут объекта внутри __init__ — если по ошибке написать self.x = Integer() внутри __init__, никакого специального поведения не возникнет: это будет обычный объект, сохранённый как обычный атрибут конкретного экземпляра, без какого-либо перехвата чтения или записи.

Итоги урока

Дескриптор — это класс, реализующий методы __get__, __set__ и, опционально, __delete__, объект которого, будучи сохранённым как атрибут класса, перехватывает все обращения к одноимённому атрибуту у объектов этого класса. Дескрипторы без __set__ (non-data descriptors) перехватывают только чтение и уступают приоритет содержимому __dict__ объекта; дескрипторы с __set__ (data descriptors) перехватывают чтение и запись безусловно, обладая приоритетом над __dict__ объекта в любом случае.

Главное практическое преимущество дескриптора перед property — возможность написать логику контроля доступа один раз и применить её к произвольному числу атрибутов и даже разных классов, не копируя код. Именно на этом механизме и построен сам property: он представляет собой встроенный, готовый к использованию data descriptor, рассчитанный на единственный атрибут одного класса. В прикладном программировании дескрипторы пишут вручную нечасто, полагаясь на property там, где переиспользование не требуется, — но на дескрипторах строится внутренняя механика многих фреймворков и библиотек, включая ORM-системы, где переиспользование логики полей между множеством моделей оправдывает дополнительную сложность.

В следующем модуле курса мы перейдём к магическим методам — тому обширному набору dunder-методов, который определяет, как объекты представляются в виде строк, сравниваются друг с другом, участвуют в арифметических операциях и ведут себя в цикле for. Понимание, разобранное в этом уроке, — то, что определённые имена методов заставляют Python вызывать их автоматически в строго предусмотренных ситуациях, — прямо продолжится и там, только на этот раз речь пойдёт не о доступе к атрибутам, а о поведении объектов в целом.


Вопросы для проверки

  1. Что делает объект дескриптором и какие методы для этого нужно реализовать?
  2. В чём разница между data descriptor и non-data descriptor?
  3. Почему дескрипторы помогают избежать дублирования кода по сравнению с property?
  4. Что делает метод __set_name__ и когда он вызывается?
  5. Почему имя, вычисляемое в __set_name__ для хранения значения, обязательно должно отличаться от имени самого дескриптора?
  6. Что произойдёт при попытке присвоить значение атрибуту, связанному с non-data descriptor?
  7. Почему data descriptor обладает более высоким приоритетом, чем non-data descriptor?
  8. Как связаны property и дескрипторы?
  9. Почему дескрипторы редко пишут вручную в прикладном коде, но активно используют в библиотеках и фреймворках?

Задачи

Задача 1.

Реализуйте дескриптор PositiveNumber, обеспечивающий, что связанное с ним значение — число (int или float), строго больше нуля. При попытке присвоить некорректное значение дескриптор должен выбрасывать ValueError. Примените дескриптор к классу Product для полей price и weight.

Пример использования:

p = Product(1000, 2.5)
print(p.price, p.weight)   # 1000 2.5

p.price = -100   # ValueError: Значение должно быть положительным числом

Задача 2.

Реализуйте дескриптор StringField(min_len, max_len), проверяющий, что значение — строка, длина которой лежит в диапазоне [min_len, max_len]. Обратите внимание, что дескриптору в этом случае нужно принимать параметры при создании — реализуйте __init__ дескриптора. Примените его к классу Comment для полей author и text с разными допустимыми диапазонами длины.

Пример использования:

class Comment:
    author = StringField(2, 50)
    text = StringField(1, 500)

    def __init__(self, author, text):
        self.author = author
        self.text = text


c = Comment("Егор", "Отличная статья!")
print(c.author, c.text)

c2 = Comment("И", "Текст")   # ValueError: слишком короткое имя автора

Задача 3.

Реализуйте дескриптор ReadOnly(value), который хранит заранее заданное значение и разрешает только чтение — при попытке присвоить что-либо через этот дескриптор должно выбрасываться исключение AttributeError. Примените его к классу Config для поля version.

Пример использования:

class Config:
    version = ReadOnly("1.0.0")


cfg = Config()
print(cfg.version)   # 1.0.0

cfg.version = "2.0.0"   # AttributeError: Это поле доступно только для чтения

Задача 4.

Реализуйте дескриптор LoggedAttribute, который при каждом чтении выводит Чтение <имя>: <значение>, а при каждой записи — Запись <имя>: <значение>, и только затем выполняет саму операцию. __set_name__ сохраняет два разных имени: public_name — «человеческое» имя атрибута для вывода в лог ('temperature'), и name — внутреннее имя для фактического хранения значения ('_temperature'). Примените дескриптор к классу Sensor для поля temperature.

Пример использования:

class Sensor:
    temperature = LoggedAttribute()

    def __init__(self, temperature):
        self.temperature = temperature


s = Sensor(20)
# Запись temperature: 20

print(s.temperature)
# Чтение temperature: 20
# 20

s.temperature = 25
# Запись temperature: 25

Задача 5.

Реализуйте дескриптор CachedProperty, вычисляющий значение только при первом обращении к нему и сохраняющий результат для всех последующих обращений (кеширование). В качестве вычисления используйте функцию, переданную дескриптору при создании. Продемонстрируйте на классе Report, где поле summary вычисляется через переданную «дорогую» функцию, вызываемую только один раз.

Пример использования:

def compute_summary(report):
    return f"Отчёт по {len(report.data)} записям"


class Report:
    def __init__(self, data):
        self.data = data

    summary = CachedProperty(compute_summary)


r = Report([1, 2, 3, 4, 5])

print(r.summary)   # Значение вычислено заново \n Отчёт по 5 записям
print(r.summary)   # Значение взято из кеша \n Отчёт по 5 записям

Задача 6.

Реализуйте дескриптор Choices(*allowed_values), ограничивающий присваиваемое значение заранее заданным набором допустимых вариантов. При попытке присвоить значение вне этого набора должно выбрасываться ValueError со списком допустимых вариантов в тексте сообщения. Примените дескриптор к классу Task для поля status с допустимыми значениями "new", "in_progress", "done".

Пример использования:

class Task:
    status = Choices("new", "in_progress", "done")

    def __init__(self, title, status="new"):
        self.title = title
        self.status = status


t = Task("Написать урок")
print(t.status)   # new

t.status = "in_progress"
print(t.status)   # in_progress

t.status = "cancelled"   # ValueError: Допустимые значения: new, in_progress, done

Предыдущий урок | Следующий урок