descriptor.md · Markdown – Документация Python
Документация Python неофициальный перевод

descriptor.md

1114 строк · 70.4 КБ · обычная страница · сырой текст · скачать

1> **Источник:** https://python-all.ru/3.12/howto/descriptor.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# [Руководство по дескрипторам](https://python-all.ru/3.12/howto/descriptor.html#id1)89**Автор:**1011Raymond Hettinger1213**Контакт:**1415\<python at rcn dot com\>1617Содержание [Руководство по дескрипторам](https://python-all.ru/3.12/howto/descriptor.html#descriptor-guide) [Введение](https://python-all.ru/3.12/howto/descriptor.html#primer) [Простой пример: дескриптор, возвращающий константу](https://python-all.ru/3.12/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant) [Динамический поиск](https://python-all.ru/3.12/howto/descriptor.html#dynamic-lookups) [Управляемые атрибуты](https://python-all.ru/3.12/howto/descriptor.html#managed-attributes) [Настраиваемые имена](https://python-all.ru/3.12/howto/descriptor.html#customized-names) [Заключительные мысли](https://python-all.ru/3.12/howto/descriptor.html#closing-thoughts) [Полный практический пример](https://python-all.ru/3.12/howto/descriptor.html#complete-practical-example) [Класс-валидатор](https://python-all.ru/3.12/howto/descriptor.html#validator-class) [Пользовательские валидаторы](https://python-all.ru/3.12/howto/descriptor.html#custom-validators) [Практическое применение](https://python-all.ru/3.12/howto/descriptor.html#practical-application) [Техническое руководство](https://python-all.ru/3.12/howto/descriptor.html#technical-tutorial) [Аннотация](https://python-all.ru/3.12/howto/descriptor.html#abstract) [Определение и введение](https://python-all.ru/3.12/howto/descriptor.html#definition-and-introduction) [Протокол дескриптора](https://python-all.ru/3.12/howto/descriptor.html#descriptor-protocol) [Обзор вызова дескриптора](https://python-all.ru/3.12/howto/descriptor.html#overview-of-descriptor-invocation) [Вызов из экземпляра](https://python-all.ru/3.12/howto/descriptor.html#invocation-from-an-instance) [Вызов из класса](https://python-all.ru/3.12/howto/descriptor.html#invocation-from-a-class) [Вызов через super](https://python-all.ru/3.12/howto/descriptor.html#invocation-from-super) [Сводка логики вызова](https://python-all.ru/3.12/howto/descriptor.html#summary-of-invocation-logic) [Автоматическое уведомление об имени](https://python-all.ru/3.12/howto/descriptor.html#automatic-name-notification) [Пример ORM](https://python-all.ru/3.12/howto/descriptor.html#orm-example) [Эквиваленты на чистом Python](https://python-all.ru/3.12/howto/descriptor.html#pure-python-equivalents) [Свойства](https://python-all.ru/3.12/howto/descriptor.html#properties) [Функции и методы](https://python-all.ru/3.12/howto/descriptor.html#functions-and-methods) [Виды методов](https://python-all.ru/3.12/howto/descriptor.html#kinds-of-methods) [Статические методы](https://python-all.ru/3.12/howto/descriptor.html#static-methods) [Методы класса](https://python-all.ru/3.12/howto/descriptor.html#class-methods) [Объекты-члены и \_\_slots\_\_](https://python-all.ru/3.12/howto/descriptor.html#member-objects-and-slots)1819[Дескрипторы](https://python-all.ru/3.12/glossary.html#term-descriptor) позволяют объектам настраивать поиск, хранение и удаление атрибутов.2021В этом руководстве четыре основных раздела:22231. «Введение» даёт базовый обзор, плавно переходя от простых примеров, добавляя по одной возможности за раз. Начните отсюда, если вы новичок в дескрипторах.242. Второй раздел показывает полный практический пример дескриптора. Если вы уже знаете основы, начинайте с него.253. Третий раздел представляет более техническое руководство, которое углубляется в детальные механизмы работы дескрипторов. Большинству людей не нужна такая степень детализации.264. Последний раздел содержит чистые аналоги Python для встроенных дескрипторов, написанных на C. Прочитайте его, если вам интересно, как функции превращаются в связанные методы или как реализованы распространённые инструменты, такие как [`classmethod()`](https://python-all.ru/3.12/library/functions.html#classmethod), [`staticmethod()`](https://python-all.ru/3.12/library/functions.html#staticmethod), [`property()`](https://python-all.ru/3.12/library/functions.html#property) и [\_\_slots\_\_](https://python-all.ru/3.12/glossary.html#term-__slots__).2728## [Введение](https://python-all.ru/3.12/howto/descriptor.html#id2)2930В этом введении мы начнём с самого простого возможного примера, а затем будем добавлять новые возможности одну за другой.3132### [Простой пример: дескриптор, возвращающий константу](https://python-all.ru/3.12/howto/descriptor.html#id3)3334Класс `Ten` – это дескриптор, у которого метод [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__) всегда возвращает константу `10`:3536```python37class Ten:38    def __get__(self, obj, objtype=None):39        return 1040```4142Чтобы использовать дескриптор, его нужно сохранить как переменную класса в другом классе:4344```python45class A:46    x = 5                       # Обычный атрибут класса47    y = Ten()                   # Экземпляр дескриптора48```4950Интерактивный сеанс показывает разницу между обычным поиском атрибута и поиском через дескриптор:5152```pycon53>>> a = A()                     # Создать экземпляр класса A54>>> a.x                         # Обычный поиск атрибута55556>>> a.y                         # Поиск через дескриптор571058```5960При поиске атрибута `a.x` оператор точки находит `'x': 5` в словаре класса. При поиске `a.y` оператор точки находит экземпляр дескриптора, распознаваемый по его методу `__get__`. Вызов этого метода возвращает `10`.6162Обратите внимание, что значение `10` не хранится ни в словаре класса, ни в словаре экземпляра. Вместо этого значение `10` вычисляется по требованию.6364Этот пример показывает, как работает простой дескриптор, но он не очень полезен. Для получения констант лучше подойдёт обычный поиск атрибута.6566В следующем разделе мы создадим нечто более полезное – динамический поиск.6768### [Динамический поиск](https://python-all.ru/3.12/howto/descriptor.html#id4)6970Интересные дескрипторы обычно выполняют вычисления вместо возврата констант:7172```python73import os7475class DirectorySize:7677    def __get__(self, obj, objtype=None):78        return len(os.listdir(obj.dirname))7980class Directory:8182    size = DirectorySize()              # Экземпляр дескриптора8384    def __init__(self, dirname):85        self.dirname = dirname          # Обычный атрибут экземпляра86```8788Интерактивный сеанс показывает, что поиск динамический – каждый раз вычисляются разные, обновлённые ответы:8990```python91>>> s = Directory('songs')92>>> g = Directory('games')93>>> s.size                              # В каталоге songs двадцать файлов942095>>> g.size                              # В каталоге games три файла96397>>> os.remove('games/chess')            # Удалить игру98>>> g.size                              # Счётчик файлов обновляется автоматически992100```101102Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример также раскрывает назначение параметров [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__). Параметр *self* – это *size*, экземпляр *DirectorySize*. Параметр *obj* – это либо *g*, либо *s*, экземпляр *Directory*. Именно параметр *obj* позволяет методу [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__) узнать целевой каталог. Параметр *objtype* – это класс *Directory*.103104### [Управляемые атрибуты](https://python-all.ru/3.12/howto/descriptor.html#id5)105106Популярное применение дескрипторов – управление доступом к данным экземпляра. Дескриптор назначается публичному атрибуту в словаре класса, в то время как фактические данные хранятся как приватный атрибут в словаре экземпляра. Методы дескриптора [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__) и [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__) вызываются при обращении к публичному атрибуту.107108В следующем примере *age* – публичный атрибут, а *\_age* – приватный атрибут. При обращении к публичному атрибуту дескриптор регистрирует поиск или обновление:109110```python111import logging112113logging.basicConfig(level=logging.INFO)114115class LoggedAgeAccess:116117    def __get__(self, obj, objtype=None):118        value = obj._age119        logging.info('Accessing %r giving %r', 'age', value)120        return value121122    def __set__(self, obj, value):123        logging.info('Updating %r to %r', 'age', value)124        obj._age = value125126class Person:127128    age = LoggedAgeAccess()             # Экземпляр дескриптора129130    def __init__(self, name, age):131        self.name = name                # Обычный атрибут экземпляра132        self.age = age                  # Вызывает __set__()133134    def birthday(self):135        self.age += 1                   # Вызывает и __get__(), и __set__()136```137138Интерактивный сеанс показывает, что весь доступ к управляемому атрибуту *age* регистрируется, а обычный атрибут *name* – нет:139140```pycon141>>> mary = Person('Mary M', 30)         # Первоначальное обновление возраста записывается142INFO:root:Updating 'age' to 30143>>> dave = Person('David D', 40)144INFO:root:Updating 'age' to 40145146>>> vars(mary)                          # Фактические данные находятся в приватном атрибуте147{'name': 'Mary M', '_age': 30}148>>> vars(dave)149{'name': 'David D', '_age': 40}150151>>> mary.age                            # Получить доступ к данным и записать поиск152INFO:root:Accessing 'age' giving 3015330154>>> mary.birthday()                     # Обновления также записываются155INFO:root:Accessing 'age' giving 30156INFO:root:Updating 'age' to 31157158>>> dave.name                           # Обычный поиск атрибута не записывается159'David D'160>>> dave.age                            # Записывается только управляемый атрибут161INFO:root:Accessing 'age' giving 4016240163```164165Одна из главных проблем этого примера – приватное имя *\_age* жёстко задано в классе *LoggedAgeAccess*. Это означает, что каждый экземпляр может иметь только один регистрируемый атрибут, и его имя нельзя изменить. В следующем примере мы исправим эту проблему.166167### [Настраиваемые имена](https://python-all.ru/3.12/howto/descriptor.html#id6)168169Когда класс использует дескрипторы, он может сообщить каждому дескриптору, какое имя переменной было использовано.170171В этом примере класс `Person` имеет два экземпляра дескриптора: *name* и *age*. Когда класс `Person` определён, он делает обратный вызов [`__set_name__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set_name__) в *LoggedAccess*, чтобы имена полей были записаны, давая каждому дескриптору свои *public\_name* и *private\_name*:172173```python174import logging175176logging.basicConfig(level=logging.INFO)177178class LoggedAccess:179180    def __set_name__(self, owner, name):181        self.public_name = name182        self.private_name = '_' + name183184    def __get__(self, obj, objtype=None):185        value = getattr(obj, self.private_name)186        logging.info('Accessing %r giving %r', self.public_name, value)187        return value188189    def __set__(self, obj, value):190        logging.info('Updating %r to %r', self.public_name, value)191        setattr(obj, self.private_name, value)192193class Person:194195    name = LoggedAccess()                # Первый экземпляр дескриптора196    age = LoggedAccess()                 # Второй экземпляр дескриптора197198    def __init__(self, name, age):199        self.name = name                 # Вызывает первый дескриптор200        self.age = age                   # Вызывает второй дескриптор201202    def birthday(self):203        self.age += 1204```205206Интерактивный сеанс показывает, что класс `Person` вызвал [`__set_name__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set_name__), чтобы имена полей были записаны. Здесь мы вызываем [`vars()`](https://python-all.ru/3.12/library/functions.html#vars) для поиска дескриптора без его активации:207208```pycon209>>> vars(vars(Person)['name'])210{'public_name': 'name', 'private_name': '_name'}211>>> vars(vars(Person)['age'])212{'public_name': 'age', 'private_name': '_age'}213```214215Новый класс теперь регистрирует доступ как к *name*, так и к *age*:216217```pycon218>>> pete = Person('Peter P', 10)219INFO:root:Updating 'name' to 'Peter P'220INFO:root:Updating 'age' to 10221>>> kate = Person('Catherine C', 20)222INFO:root:Updating 'name' to 'Catherine C'223INFO:root:Updating 'age' to 20224```225226Два экземпляра *Person* содержат только приватные имена:227228```pycon229>>> vars(pete)230{'_name': 'Peter P', '_age': 10}231>>> vars(kate)232{'_name': 'Catherine C', '_age': 20}233```234235### [Заключительные мысли](https://python-all.ru/3.12/howto/descriptor.html#id7)236237[Дескриптором](https://python-all.ru/3.12/glossary.html#term-descriptor) мы называем любой объект, который определяет [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__) или [`__delete__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__delete__).238239Дескрипторы могут дополнительно иметь метод [`__set_name__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set_name__). Он используется только в случаях, когда дескриптору нужно знать либо класс, в котором он был создан, либо имя переменной класса, которой он был присвоен. (Если этот метод присутствует, он вызывается, даже если класс не является дескриптором.)240241Дескрипторы вызываются оператором точки при поиске атрибута. Если дескриптор доступен косвенно через `vars(some_class)[descriptor_name]`, возвращается экземпляр дескриптора без его вызова.242243Дескрипторы работают только при использовании в качестве переменных класса. Если их поместить в экземпляры, они не оказывают никакого эффекта.244245Основная мотивация дескрипторов – предоставить точку входа, позволяющую объектам, хранящимся в переменных класса, контролировать то, что происходит во время поиска атрибута.246247Традиционно вызывающий класс контролирует то, что происходит во время поиска. Дескрипторы инвертируют это отношение и позволяют данным, к которым осуществляется поиск, влиять на процесс.248249Дескрипторы используются повсеместно в языке. Именно так функции превращаются в связанные методы. Распространенные инструменты, такие как [`classmethod()`](https://python-all.ru/3.12/library/functions.html#classmethod), [`staticmethod()`](https://python-all.ru/3.12/library/functions.html#staticmethod), [`property()`](https://python-all.ru/3.12/library/functions.html#property) и [`functools.cached_property()`](https://python-all.ru/3.12/library/functools.html#functools.cached_property), все реализованы как дескрипторы.250251## [Полный практический пример](https://python-all.ru/3.12/howto/descriptor.html#id8)252253В этом примере создается практичный и мощный инструмент для поиска пресловутых труднообнаруживаемых ошибок повреждения данных.254255### [Класс Validator](https://python-all.ru/3.12/howto/descriptor.html#id9)256257Валидатор – это дескриптор для управляемого доступа к атрибутам. Перед сохранением любых данных он проверяет, соответствует ли новое значение различным ограничениям типа и диапазона. Если эти ограничения не соблюдены, вызывается исключение, чтобы предотвратить повреждение данных на раннем этапе.258259Этот класс `Validator` является одновременно [абстрактным базовым классом](https://python-all.ru/3.12/glossary.html#term-abstract-base-class) и дескриптором управляемого атрибута:260261```python262from abc import ABC, abstractmethod263264class Validator(ABC):265266    def __set_name__(self, owner, name):267        self.private_name = '_' + name268269    def __get__(self, obj, objtype=None):270        return getattr(obj, self.private_name)271272    def __set__(self, obj, value):273        self.validate(value)274        setattr(obj, self.private_name, value)275276    @abstractmethod277    def validate(self, value):278        pass279```280281Пользовательские валидаторы должны наследовать от `Validator` и предоставлять метод `validate()` для проверки различных ограничений по мере необходимости.282283### [Пользовательские валидаторы](https://python-all.ru/3.12/howto/descriptor.html#id10)284285Вот три практические утилиты для проверки данных:2862871. `OneOf` проверяет, что значение является одним из ограниченного набора опций.2882. `Number` проверяет, что значение является либо [`int`](https://python-all.ru/3.12/library/functions.html#int), либо [`float`](https://python-all.ru/3.12/library/functions.html#float). Опционально он проверяет, что значение находится между заданным минимумом или максимумом.2893. `String` проверяет, что значение является [`str`](https://python-all.ru/3.12/library/stdtypes.html#str). Опционально он проверяет заданную минимальную или максимальную длину. Он также может проверять определенный пользователем [предикат](https://python-all.ru/3.12/howto/descriptor.html).290291```python292class OneOf(Validator):293294    def __init__(self, *options):295        self.options = set(options)296297    def validate(self, value):298        if value not in self.options:299            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')300301class Number(Validator):302303    def __init__(self, minvalue=None, maxvalue=None):304        self.minvalue = minvalue305        self.maxvalue = maxvalue306307    def validate(self, value):308        if not isinstance(value, (int, float)):309            raise TypeError(f'Expected {value!r} to be an int or float')310        if self.minvalue is not None and value < self.minvalue:311            raise ValueError(312                f'Expected {value!r} to be at least {self.minvalue!r}'313            )314        if self.maxvalue is not None and value > self.maxvalue:315            raise ValueError(316                f'Expected {value!r} to be no more than {self.maxvalue!r}'317            )318319class String(Validator):320321    def __init__(self, minsize=None, maxsize=None, predicate=None):322        self.minsize = minsize323        self.maxsize = maxsize324        self.predicate = predicate325326    def validate(self, value):327        if not isinstance(value, str):328            raise TypeError(f'Expected {value!r} to be an str')329        if self.minsize is not None and len(value) < self.minsize:330            raise ValueError(331                f'Expected {value!r} to be no smaller than {self.minsize!r}'332            )333        if self.maxsize is not None and len(value) > self.maxsize:334            raise ValueError(335                f'Expected {value!r} to be no bigger than {self.maxsize!r}'336            )337        if self.predicate is not None and not self.predicate(value):338            raise ValueError(339                f'Expected {self.predicate} to be true for {value!r}'340            )341```342343### [Практическое применение](https://python-all.ru/3.12/howto/descriptor.html#id11)344345Вот как валидаторы данных могут быть использованы в реальном классе:346347```python348class Component:349350    name = String(minsize=3, maxsize=10, predicate=str.isupper)351    kind = OneOf('wood', 'metal', 'plastic')352    quantity = Number(minvalue=0)353354    def __init__(self, name, kind, quantity):355        self.name = name356        self.kind = kind357        self.quantity = quantity358```359360Дескрипторы предотвращают создание недопустимых экземпляров:361362```pycon363>>> Component('Widget', 'metal', 5)      # Заблокировано: 'Widget' не в верхнем регистре364Traceback (most recent call last):365    ...366ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'367368>>> Component('WIDGET', 'metle', 5)      # Заблокировано: 'metle' написано с ошибкой369Traceback (most recent call last):370    ...371ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}372373>>> Component('WIDGET', 'metal', -5)     # Заблокировано: -5 отрицательное374Traceback (most recent call last):375    ...376ValueError: Expected -5 to be at least 0377>>> Component('WIDGET', 'metal', 'V')    # Заблокировано: 'V' не является числом378Traceback (most recent call last):379    ...380TypeError: Expected 'V' to be an int or float381382>>> c = Component('WIDGET', 'metal', 5)  # Разрешено: входные данные корректны383```384385## [Техническое руководство](https://python-all.ru/3.12/howto/descriptor.html#id12)386387Далее следует более техническое руководство по механике и деталям того, как работают дескрипторы.388389### [Аннотация](https://python-all.ru/3.12/howto/descriptor.html#id13)390391Определяет дескрипторы, обобщает протокол и показывает, как дескрипторы вызываются. Приводится пример, показывающий, как работают объектно-реляционные отображения.392393Изучение дескрипторов не только открывает доступ к более широкому набору инструментов, но и дает более глубокое понимание того, как работает Python.394395### [Определение и введение](https://python-all.ru/3.12/howto/descriptor.html#id14)396397В общем случае дескриптор – это значение атрибута, которое имеет один из методов протокола дескриптора. Эти методы: [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__), и [`__delete__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__delete__). Если для атрибута определен хотя бы один из этих методов, то такой атрибут называется [дескриптором](https://python-all.ru/3.12/glossary.html#term-descriptor).398399Поведение по умолчанию при доступе к атрибуту – получить, установить или удалить атрибут из словаря объекта. Например, `a.x` имеет цепочку поиска, начиная с `a.__dict__['x']`, затем `type(a).__dict__['x']` и продолжая порядком разрешения методов `type(a)`. Если найденное значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. Где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены.400401Дескрипторы – это мощный протокол общего назначения. Они являются механизмом, лежащим в основе свойств, методов, статических методов, методов класса и [`super()`](https://python-all.ru/3.12/library/functions.html#super). Они используются во всем Python. Дескрипторы упрощают базовый код на C и предоставляют гибкий набор новых инструментов для повседневных программ на Python.402403### [Протокол дескриптора](https://python-all.ru/3.12/howto/descriptor.html#id15)404405`descr.__get__(self, obj, type=None)`406407`descr.__set__(self, obj, value)`408409`descr.__delete__(self, obj)`410411Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.412413Если объект определяет [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__) или [`__delete__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__delete__), он считается дескриптором данных. Дескрипторы, которые определяют только [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), называются дескрипторами, не являющимися дескрипторами данных (они часто используются для методов, но возможно и другое применение).414415Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.416417Чтобы создать дескриптор данных только для чтения, определите и [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__), причём [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__) должен возбуждать [`AttributeError`](https://python-all.ru/3.12/library/exceptions.html#AttributeError) при вызове. Определения метода [`__set__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set__) с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.418419### [Обзор вызова дескрипторов](https://python-all.ru/3.12/howto/descriptor.html#id16)420421Дескриптор можно вызвать напрямую с помощью `desc.__get__(obj)` или `desc.__get__(None, cls)`.422423Но чаще дескриптор вызывается автоматически при доступе к атрибуту.424425Выражение `obj.x` выполняет поиск атрибута `x` в цепочке пространств имён для `obj`. Если поиск находит дескриптор вне [`__dict__`](https://python-all.ru/3.12/reference/datamodel.html#object.__dict__) экземпляра, вызывается его метод [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__) в соответствии с правилами приоритета, перечисленными ниже.426427Детали вызова зависят от того, является ли `obj` объектом, классом или экземпляром super.428429### [Вызов из экземпляра](https://python-all.ru/3.12/howto/descriptor.html#id17)430431Поиск в экземпляре просматривает цепочку пространств имён, отдавая наивысший приоритет дескрипторам данных, затем переменным экземпляра, затем дескрипторам не-данных, затем переменным класса и, наконец, [`__getattr__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattr__), если он предусмотрен.432433Если дескриптор найден для `a.x`, он вызывается с: `desc.__get__(a, type(a))`.434435Логика точечного поиска находится в [`object.__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__). Вот эквивалент на чистом Python:436437```python438def find_name_in_mro(cls, name, default):439    "Emulate _PyType_Lookup() in Objects/typeobject.c"440    for base in cls.__mro__:441        if name in vars(base):442            return vars(base)[name]443    return default444445def object_getattribute(obj, name):446    "Emulate PyObject_GenericGetAttr() in Objects/object.c"447    null = object()448    objtype = type(obj)449    cls_var = find_name_in_mro(objtype, name, null)450    descr_get = getattr(type(cls_var), '__get__', null)451    if descr_get is not null:452        if (hasattr(type(cls_var), '__set__')453            or hasattr(type(cls_var), '__delete__')):454            return descr_get(cls_var, obj, objtype)     # дескриптор данных455    if hasattr(obj, '__dict__') and name in vars(obj):456        return vars(obj)[name]                          # переменная экземпляра457    if descr_get is not null:458        return descr_get(cls_var, obj, objtype)         # дескриптор без данных459    if cls_var is not null:460        return cls_var                                  # переменная класса461    raise AttributeError(name)462```463464Обратите внимание: в коде [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) нет ловушки [`__getattr__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattr__). Именно поэтому прямой вызов [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) или вызов с `super().__getattribute__` полностью обходит [`__getattr__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattr__).465466Вместо этого именно оператор точки и функция [`getattr()`](https://python-all.ru/3.12/library/functions.html#getattr) отвечают за вызов [`__getattr__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattr__) всякий раз, когда [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) возбуждает [`AttributeError`](https://python-all.ru/3.12/library/exceptions.html#AttributeError). Их логика инкапсулирована во вспомогательной функции:467468```python469def getattr_hook(obj, name):470    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"471    try:472        return obj.__getattribute__(name)473    except AttributeError:474        if not hasattr(type(obj), '__getattr__'):475            raise476    return type(obj).__getattr__(obj, name)             # getattr477```478479### [Вызов из класса](https://python-all.ru/3.12/howto/descriptor.html#id18)480481Логика точечного поиска, такого как `A.x`, находится в `type.__getattribute__()`. Шаги аналогичны шагам для `object.__getattribute__()`, но поиск по словарю экземпляра заменяется поиском по [порядку разрешения методов](https://python-all.ru/3.12/glossary.html#term-method-resolution-order) класса.482483Если дескриптор найден, он вызывается с `desc.__get__(None, A)`.484485Полную реализацию на C можно найти в `type_getattro()` и `_PyType_Lookup()` в [Objects/typeobject.c](https://python-all.ru/src/3.12/Objects/typeobject.c).486487### [Вызов из super](https://python-all.ru/3.12/howto/descriptor.html#id19)488489Логика точечного поиска в super находится в методе [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) объекта, возвращаемого [`super()`](https://python-all.ru/3.12/library/functions.html#super).490491Точечный поиск, такой как `super(A, obj).m`, ищет в `obj.__class__.__mro__` базовый класс `B`, следующий сразу за `A`, и затем возвращает `B.__dict__['m'].__get__(obj, A)`. Если это не дескриптор, `m` возвращается без изменений.492493Полную реализацию на C можно найти в `super_getattro()` в [Objects/typeobject.c](https://python-all.ru/src/3.12/Objects/typeobject.c). Эквивалент на чистом Python можно найти в [руководстве Гвидо](https://python-all.ru/3.12/howto/descriptor.html).494495### [Сводка по логике вызова](https://python-all.ru/3.12/howto/descriptor.html#id20)496497Механизм дескрипторов встроен в методы [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) для [`object`](https://python-all.ru/3.12/library/functions.html#object), [`type`](https://python-all.ru/3.12/library/functions.html#type) и [`super()`](https://python-all.ru/3.12/library/functions.html#super).498499Важно запомнить следующее:500501- Дескрипторы вызываются методом [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__).502- Классы наследуют этот механизм от [`object`](https://python-all.ru/3.12/library/functions.html#object), [`type`](https://python-all.ru/3.12/library/functions.html#type) или [`super()`](https://python-all.ru/3.12/library/functions.html#super).503- Переопределение [`__getattribute__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__getattribute__) предотвращает автоматические вызовы дескрипторов, поскольку вся логика дескрипторов находится в этом методе.504- `object.__getattribute__()` и `type.__getattribute__()` по-разному вызывают [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__). Первый включает экземпляр и может включать класс. Второй передаёт `None` в качестве экземпляра и всегда включает класс.505- Дескрипторы данных всегда переопределяют словари экземпляров.506- Дескрипторы не-данных могут быть переопределены словарями экземпляров.507508### [Автоматическое уведомление об имени](https://python-all.ru/3.12/howto/descriptor.html#id21)509510Иногда желательно, чтобы дескриптор знал имя переменной класса, которой он был присвоен. Когда создаётся новый класс, метакласс [`type`](https://python-all.ru/3.12/library/functions.html#type) сканирует словарь нового класса. Если какие-либо записи являются дескрипторами и в них определён [`__set_name__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set_name__), этот метод вызывается с двумя аргументами. *Владелец* – это класс, в котором используется дескриптор, а *имя* – это переменная класса, которой был присвоен дескриптор.511512Детали реализации находятся в `type_new()` и `set_names()` в [Objects/typeobject.c](https://python-all.ru/src/3.12/Objects/typeobject.c).513514Поскольку логика обновления находится в `type.__new__()`, уведомления происходят только в момент создания класса. Если дескрипторы добавляются в класс после этого, [`__set_name__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__set_name__) потребуется вызвать вручную.515516### [Пример ORM](https://python-all.ru/3.12/howto/descriptor.html#id22)517518Следующий код – это упрощённый каркас, показывающий, как дескрипторы данных можно использовать для реализации [объектно-реляционного отображения (ORM)](https://python-all.ru/3.12/howto/descriptor.html).519520Основная идея в том, что данные хранятся во внешней базе данных. Экземпляры Python хранят только ключи к таблицам базы. Дескрипторы берут на себя поиск или обновление:521522```python523class Field:524525    def __set_name__(self, owner, name):526        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'527        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'528529    def __get__(self, obj, objtype=None):530        return conn.execute(self.fetch, [obj.key]).fetchone()[0]531532    def __set__(self, obj, value):533        conn.execute(self.store, [value, obj.key])534        conn.commit()535```536537Можно использовать класс `Field` для определения [моделей](https://python-all.ru/3.12/howto/descriptor.html), описывающих схему для каждой таблицы в базе данных:538539```python540class Movie:541    table = 'Movies'                    # Имя таблицы542    key = 'title'                       # Первичный ключ543    director = Field()544    year = Field()545546    def __init__(self, key):547        self.key = key548549class Song:550    table = 'Music'551    key = 'title'552    artist = Field()553    year = Field()554    genre = Field()555556    def __init__(self, key):557        self.key = key558```559560Чтобы использовать модели, сначала нужно подключиться к базе данных:561562```python563>>> import sqlite3564>>> conn = sqlite3.connect('entertainment.db')565```566567Интерактивный сеанс показывает, как данные извлекаются из базы данных и как их можно обновить:568569```pycon570>>> Movie('Star Wars').director571'George Lucas'572>>> jaws = Movie('Jaws')573>>> f'Released in {jaws.year} by {jaws.director}'574'Released in 1975 by Steven Spielberg'575576>>> Song('Country Roads').artist577'John Denver'578579>>> Movie('Star Wars').director = 'J.J. Abrams'580>>> Movie('Star Wars').director581'J.J. Abrams'582```583584## [Эквиваленты на чистом Python](https://python-all.ru/3.12/howto/descriptor.html#id23)585586Протокол дескрипторов прост и открывает захватывающие возможности. Несколько вариантов использования настолько распространены, что они уже встроены в стандартные инструменты. Свойства (properties), связанные методы, статические методы, методы класса и \_\_slots\_\_ – все они основаны на протоколе дескрипторов.587588### [Свойства](https://python-all.ru/3.12/howto/descriptor.html#id24)589590Вызов [`property()`](https://python-all.ru/3.12/library/functions.html#property) – это краткий способ создания дескриптора данных, который вызывает функцию при доступе к атрибуту. Его сигнатура:591592```python593property(fget=None, fset=None, fdel=None, doc=None) -> property594```595596В документации показан типичный пример использования для определения управляемого атрибута `x`:597598```python599class C:600    def getx(self): return self.__x601    def setx(self, value): self.__x = value602    def delx(self): del self.__x603    x = property(getx, setx, delx, "I'm the 'x' property.")604```605606Чтобы увидеть, как [`property()`](https://python-all.ru/3.12/library/functions.html#property) реализуется через протокол дескрипторов, вот эквивалент на чистом Python:607608```python609class Property:610    "Emulate PyProperty_Type() in Objects/descrobject.c"611612    def __init__(self, fget=None, fset=None, fdel=None, doc=None):613        self.fget = fget614        self.fset = fset615        self.fdel = fdel616        if doc is None and fget is not None:617            doc = fget.__doc__618        self.__doc__ = doc619        self._name = ''620621    def __set_name__(self, owner, name):622        self._name = name623624    def __get__(self, obj, objtype=None):625        if obj is None:626            return self627        if self.fget is None:628            raise AttributeError(629                f'property {self._name!r} of {type(obj).__name__!r} object has no getter'630             )631        return self.fget(obj)632633    def __set__(self, obj, value):634        if self.fset is None:635            raise AttributeError(636                f'property {self._name!r} of {type(obj).__name__!r} object has no setter'637             )638        self.fset(obj, value)639640    def __delete__(self, obj):641        if self.fdel is None:642            raise AttributeError(643                f'property {self._name!r} of {type(obj).__name__!r} object has no deleter'644             )645        self.fdel(obj)646647    def getter(self, fget):648        prop = type(self)(fget, self.fset, self.fdel, self.__doc__)649        prop._name = self._name650        return prop651652    def setter(self, fset):653        prop = type(self)(self.fget, fset, self.fdel, self.__doc__)654        prop._name = self._name655        return prop656657    def deleter(self, fdel):658        prop = type(self)(self.fget, self.fset, fdel, self.__doc__)659        prop._name = self._name660        return prop661```662663Встроенная функция [`property()`](https://python-all.ru/3.12/library/functions.html#property) помогает, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения требуют вмешательства метода.664665Например, класс электронной таблицы может предоставлять доступ к значению ячейки через `Cell('b10').value`. Последующие улучшения программы требуют пересчёта ячейки при каждом доступе; однако программист не хочет влиять на существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту значения в дескриптор данных свойства:666667```python668class Cell:669    ...670671    @property672    def value(self):673        "Recalculate the cell before returning value"674        self.recalc()675        return self._value676```677678В этом примере подойдёт как встроенная [`property()`](https://python-all.ru/3.12/library/functions.html#property), так и наш эквивалент `Property()`.679680### [Функции и методы](https://python-all.ru/3.12/howto/descriptor.html#id25)681682Объектно-ориентированные возможности Python построены на функциональной основе. С помощью дескрипторов, не являющихся дескрипторами данных, эти две парадигмы объединяются бесшовно.683684Функции, хранящиеся в словарях классов, превращаются в методы при вызове. Методы отличаются от обычных функций только тем, что экземпляр объекта добавляется перед остальными аргументами. По соглашению экземпляр называется *self*, но может называться *this* или любым другим именем переменной.685686Методы можно создавать вручную с помощью [`types.MethodType`](https://python-all.ru/3.12/library/types.html#types.MethodType), что примерно эквивалентно:687688```python689class MethodType:690    "Emulate PyMethod_Type in Objects/classobject.c"691692    def __init__(self, func, obj):693        self.__func__ = func694        self.__self__ = obj695696    def __call__(self, *args, **kwargs):697        func = self.__func__698        obj = self.__self__699        return func(obj, *args, **kwargs)700```701702Для поддержки автоматического создания методов функции включают метод [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__) для привязки методов при доступе к атрибуту. Это означает, что функции являются дескрипторами, не являющимися дескрипторами данных, которые возвращают связанные методы при точечном поиске от экземпляра. Вот как это работает:703704```python705class Function:706    ...707708    def __get__(self, obj, objtype=None):709        "Simulate func_descr_get() in Objects/funcobject.c"710        if obj is None:711            return self712        return MethodType(self, obj)713```714715Запуск следующего класса в интерпретаторе показывает, как дескриптор функции работает на практике:716717```python718class D:719    def f(self, x):720         return x721```722723Функция имеет атрибут [полное имя](https://python-all.ru/3.12/glossary.html#term-qualified-name) для поддержки интроспекции:724725```pycon726>>> D.f.__qualname__727'D.f'728```729730Доступ к функции через словарь класса не вызывает [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__). Вместо этого просто возвращается исходный объект функции:731732```python733>>> D.__dict__['f']734<function D.f at 0x00C45070>735```736737Точечный доступ от класса вызывает [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), который просто возвращает исходную функцию без изменений:738739```python740>>> D.f741<function D.f at 0x00C45070>742```743744Интересное поведение происходит при точечном доступе от экземпляра. Точечный поиск вызывает [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), который возвращает объект связанного метода:745746```python747>>> d = D()748>>> d.f749<bound method D.f of <__main__.D object at 0x00B18C90>>750```751752Внутренне связанный метод хранит исходную функцию и связанный экземпляр:753754```python755>>> d.f.__func__756<function D.f at 0x00C45070>757758>>> d.f.__self__759<__main__.D object at 0x00B18C90>760```761762Если вы когда-нибудь задавались вопросом, откуда берётся *self* в обычных методах или откуда *cls* в методах класса – вот ответ!763764### [Виды методов](https://python-all.ru/3.12/howto/descriptor.html#id26)765766Дескрипторы, не являющиеся дескрипторами данных, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.767768Повторим: функции имеют метод [`__get__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__get__), чтобы их можно было преобразовать в метод при доступе как атрибуты. Дескриптор, не являющийся дескриптором данных, преобразует вызов `obj.f(*args)` в `f(obj, *args)`. Вызов `cls.f(*args)` становится `f(*args)`.769770Эта таблица обобщает привязку и два наиболее полезных её варианта:771772> | Преобразование | Вызывается от объекта | Вызывается от класса |773> | --- | --- | --- |774> | функция | f(obj, \*args) | f(\*args) |775> | staticmethod | f(\*args) | f(\*args) |776> | classmethod | f(type(obj), \*args) | f(cls, \*args) |777778### [Статические методы](https://python-all.ru/3.12/howto/descriptor.html#id27)779780Статические методы возвращают базовую функцию без изменений. Вызов `c.f` или `C.f` эквивалентен прямому поиску в `object.__getattribute__(c, "f")` или `object.__getattribute__(C, "f")`. В результате функция становится одинаково доступной как из объекта, так и из класса.781782Хорошими кандидатами для статических методов являются те, которые не ссылаются на переменную `self`.783784Например, в пакете статистики может быть класс-контейнер для экспериментальных данных. Класс предоставляет обычные методы для вычисления среднего, медианы и других описательных статистик, которые зависят от данных. Однако могут быть полезные функции, которые концептуально связаны, но не зависят от данных. Например, `erf(x)` – это удобная процедура преобразования, которая встречается в статистической работе, но не зависит напрямую от конкретного набора данных. Её можно вызвать как от объекта, так и от класса: `s.erf(1.5) --> .9332` или `Sample.erf(1.5) --> .9332`.785786Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов ничем не примечательны:787788```python789class E:790    @staticmethod791    def f(x):792        return x * 10793```794795```pycon796>>> E.f(3)79730798>>> E().f(3)79930800```801802Используя протокол дескриптора без данных, чистая версия [`staticmethod()`](https://python-all.ru/3.12/library/functions.html#staticmethod) на Python выглядела бы так:803804```python805import functools806807class StaticMethod:808    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"809810    def __init__(self, f):811        self.f = f812        functools.update_wrapper(self, f)813814    def __get__(self, obj, objtype=None):815        return self.f816817    def __call__(self, *args, **kwds):818        return self.f(*args, **kwds)819```820821Вызов [`functools.update_wrapper()`](https://python-all.ru/3.12/library/functools.html#functools.update_wrapper) добавляет атрибут `__wrapped__`, который ссылается на базовую функцию. Кроме того, он переносит атрибуты, необходимые для того, чтобы обёртка выглядела как обёрнутая функция: [`__name__`](https://python-all.ru/3.12/reference/datamodel.html#function.__name__), [`__qualname__`](https://python-all.ru/3.12/reference/datamodel.html#function.__qualname__), [`__doc__`](https://python-all.ru/3.12/reference/datamodel.html#function.__doc__) и [`__annotations__`](https://python-all.ru/3.12/reference/datamodel.html#function.__annotations__).822823### [Методы класса](https://python-all.ru/3.12/howto/descriptor.html#id28)824825В отличие от статических методов, методы класса перед вызовом функции добавляют ссылку на класс в начало списка аргументов. Этот формат одинаков как для вызова от объекта, так и от класса:826827```python828class F:829    @classmethod830    def f(cls, x):831        return cls.__name__, x832```833834```pycon835>>> F.f(3)836('F', 3)837>>> F().f(3)838('F', 3)839```840841Такое поведение полезно, когда методу нужна только ссылка на класс и он не зависит от данных, хранящихся в конкретном экземпляре. Одно из применений методов класса – создание альтернативных конструкторов класса. Например, метод класса [`dict.fromkeys()`](https://python-all.ru/3.12/library/stdtypes.html#dict.fromkeys) создаёт новый словарь из списка ключей. Эквивалент на чистом Python выглядит так:842843```python844class Dict(dict):845    @classmethod846    def fromkeys(cls, iterable, value=None):847        "Emulate dict_fromkeys() in Objects/dictobject.c"848        d = cls()849        for key in iterable:850            d[key] = value851        return d852```853854Теперь новый словарь уникальных ключей можно создать так:855856```pycon857>>> d = Dict.fromkeys('abracadabra')858>>> type(d) is Dict859True860>>> d861{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}862```863864Используя протокол дескриптора без данных, чистая версия [`classmethod()`](https://python-all.ru/3.12/library/functions.html#classmethod) на Python выглядела бы так:865866```python867import functools868869class ClassMethod:870    "Emulate PyClassMethod_Type() in Objects/funcobject.c"871872    def __init__(self, f):873        self.f = f874        functools.update_wrapper(self, f)875876    def __get__(self, obj, cls=None):877        if cls is None:878            cls = type(obj)879        if hasattr(type(self.f), '__get__'):880            # Этот код добавлен в Python 3.9881            # и устарел в Python 3.11.882            return self.f.__get__(cls, cls)883        return MethodType(self.f, cls)884```885886Путь выполнения для `hasattr(type(self.f), '__get__')` был добавлен в Python 3.9 и позволяет [`classmethod()`](https://python-all.ru/3.12/library/functions.html#classmethod) поддерживать цепочки декораторов. Например, classmethod и property можно объединить в цепочку. В Python 3.11 эта функциональность была объявлена устаревшей.887888```python889class G:890    @classmethod891    @property892    def __doc__(cls):893        return f'A doc for {cls.__name__!r}'894```895896```pycon897>>> G.__doc__898"A doc for 'G'"899```900901Вызов [`functools.update_wrapper()`](https://python-all.ru/3.12/library/functools.html#functools.update_wrapper) в `ClassMethod` добавляет атрибут `__wrapped__`, который ссылается на базовую функцию. Он также переносит атрибуты, необходимые для того, чтобы обёртка выглядела как обёрнутая функция: [`__name__`](https://python-all.ru/3.12/reference/datamodel.html#function.__name__), [`__qualname__`](https://python-all.ru/3.12/reference/datamodel.html#function.__qualname__), [`__doc__`](https://python-all.ru/3.12/reference/datamodel.html#function.__doc__) и [`__annotations__`](https://python-all.ru/3.12/reference/datamodel.html#function.__annotations__).902903### [Объекты-члены и \_\_slots\_\_](https://python-all.ru/3.12/howto/descriptor.html#id29)904905Когда класс определяет `__slots__`, он заменяет словари экземпляров массивом фиксированной длины для значений слотов. С точки зрения пользователя это имеет несколько последствий:9069071. Обеспечивает немедленное обнаружение ошибок из-за опечаток в именах атрибутов. Разрешены только имена атрибутов, указанные в `__slots__`:908909```python910class Vehicle:911    __slots__ = ('id_number', 'make', 'model')912```913914```pycon915>>> auto = Vehicle()916>>> auto.id_nubmer = 'VYE483814LQEX'917Traceback (most recent call last):918    ...919AttributeError: 'Vehicle' object has no attribute 'id_nubmer'920```9219222. Помогает создавать неизменяемые объекты, где дескрипторы управляют доступом к приватным атрибутам, хранящимся в `__slots__`:923924```python925class Immutable:926927    __slots__ = ('_dept', '_name')          # Замена словаря экземпляра928929    def __init__(self, dept, name):930        self._dept = dept                   # Сохранение в частный атрибут931        self._name = name                   # Сохранение в частный атрибут932933    @property                               # Дескриптор только для чтения934    def dept(self):935        return self._dept936937    @property938    def name(self):                         # Дескриптор только для чтения939        return self._name940```941942```pycon943>>> mark = Immutable('Botany', 'Mark Watney')944>>> mark.dept945'Botany'946>>> mark.dept = 'Space Pirate'947Traceback (most recent call last):948    ...949AttributeError: property 'dept' of 'Immutable' object has no setter950>>> mark.location = 'Mars'951Traceback (most recent call last):952    ...953AttributeError: 'Immutable' object has no attribute 'location'954```9559563. Экономит память. В 64-битной сборке Linux экземпляр с двумя атрибутами занимает 48 байт с `__slots__` и 152 байта без него. Этот [шаблон «приспособленец»](https://python-all.ru/3.12/howto/descriptor.html), вероятно, имеет значение только при создании большого количества экземпляров.9579584. Увеличивает скорость. Чтение переменных экземпляра происходит на 35% быстрее с `__slots__` (согласно измерениям на Python 3.10 на процессоре Apple M1).9599605. Блокирует инструменты вроде [`functools.cached_property()`](https://python-all.ru/3.12/library/functools.html#functools.cached_property), которым для корректной работы требуется словарь экземпляра:961962```python963from functools import cached_property964965class CP:966    __slots__ = ()                          # Устраняет словарь экземпляра967968    @cached_property                        # Требуется словарь экземпляра969    def pi(self):970        return 4 * sum((-1.0)**n / (2.0*n + 1.0)971                       for n in reversed(range(100_000)))972```973974```pycon975>>> CP().pi976Traceback (most recent call last):977  ...978TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.979```980981Невозможно создать точную замену `__slots__` на чистом Python, поскольку для этого требуется прямой доступ к C-структурам и управление выделением памяти объектов. Однако можно построить достаточно точную симуляцию, где реальная C-структура для слотов эмулируется частным списком `_slotvalues`. Чтение и запись в эту частную структуру управляются дескрипторами членов:982983```python984null = object()985986class Member:987988    def __init__(self, name, clsname, offset):989        'Emulate PyMemberDef in Include/structmember.h'990        # См. также descr_new() в Objects/descrobject.c991        self.name = name992        self.clsname = clsname993        self.offset = offset994995    def __get__(self, obj, objtype=None):996        'Emulate member_get() in Objects/descrobject.c'997        # См. также PyMember_GetOne() в Python/structmember.c998        if obj is None:999            return self1000        value = obj._slotvalues[self.offset]1001        if value is null:1002            raise AttributeError(self.name)1003        return value10041005    def __set__(self, obj, value):1006        'Emulate member_set() in Objects/descrobject.c'1007        obj._slotvalues[self.offset] = value10081009    def __delete__(self, obj):1010        'Emulate member_delete() in Objects/descrobject.c'1011        value = obj._slotvalues[self.offset]1012        if value is null:1013            raise AttributeError(self.name)1014        obj._slotvalues[self.offset] = null10151016    def __repr__(self):1017        'Emulate member_repr() in Objects/descrobject.c'1018        return f'<Member {self.name!r} of {self.clsname!r}>'1019```10201021Метод `type.__new__()` отвечает за добавление объектов-членов в переменные класса:10221023```python1024class Type(type):1025    'Simulate how the type metaclass adds member objects for slots'10261027    def __new__(mcls, clsname, bases, mapping, **kwargs):1028        'Emulate type_new() in Objects/typeobject.c'1029        # type_new() вызывает PyTypeReady(), который вызывает add_methods()1030        slot_names = mapping.get('slot_names', [])1031        for offset, name in enumerate(slot_names):1032            mapping[name] = Member(name, clsname, offset)1033        return type.__new__(mcls, clsname, bases, mapping, **kwargs)1034```10351036Метод [`object.__new__()`](https://python-all.ru/3.12/reference/datamodel.html#object.__new__) отвечает за создание экземпляров, которые имеют слоты вместо словаря экземпляра. Вот примерная симуляция на чистом Python:10371038```python1039class Object:1040    'Simulate how object.__new__() allocates memory for __slots__'10411042    def __new__(cls, *args, **kwargs):1043        'Emulate object_new() in Objects/typeobject.c'1044        inst = super().__new__(cls)1045        if hasattr(cls, 'slot_names'):1046            empty_slots = [null] * len(cls.slot_names)1047            object.__setattr__(inst, '_slotvalues', empty_slots)1048        return inst10491050    def __setattr__(self, name, value):1051        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'1052        cls = type(self)1053        if hasattr(cls, 'slot_names') and name not in cls.slot_names:1054            raise AttributeError(1055                f'{cls.__name__!r} object has no attribute {name!r}'1056            )1057        super().__setattr__(name, value)10581059    def __delattr__(self, name):1060        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'1061        cls = type(self)1062        if hasattr(cls, 'slot_names') and name not in cls.slot_names:1063            raise AttributeError(1064                f'{cls.__name__!r} object has no attribute {name!r}'1065            )1066        super().__delattr__(name)1067```10681069Чтобы использовать симуляцию в реальном классе, достаточно унаследоваться от `Object` и установить [метакласс](https://python-all.ru/3.12/glossary.html#term-metaclass) в `Type`:10701071```python1072class H(Object, metaclass=Type):1073    'Instance variables stored in slots'10741075    slot_names = ['x', 'y']10761077    def __init__(self, x, y):1078        self.x = x1079        self.y = y1080```10811082На этом этапе метакласс загрузил объекты-члены для *x* и *y*:10831084```python1085>>> from pprint import pp1086>>> pp(dict(vars(H)))1087{'__module__': '__main__',1088 '__doc__': 'Instance variables stored in slots',1089 'slot_names': ['x', 'y'],1090 '__init__': <function H.__init__ at 0x7fb5d302f9d0>,1091 'x': <Member 'x' of 'H'>,1092 'y': <Member 'y' of 'H'>}1093```10941095При создании экземпляров у них есть список `slot_values`, в котором хранятся атрибуты:10961097```pycon1098>>> h = H(10, 20)1099>>> vars(h)1100{'_slotvalues': [10, 20]}1101>>> h.x = 551102>>> vars(h)1103{'_slotvalues': [55, 20]}1104```11051106Атрибуты с ошибками в написании или не назначенные атрибуты вызовут исключение:11071108```pycon1109>>> h.xz1110Traceback (most recent call last):1111    ...1112AttributeError: 'H' object has no attribute 'xz'1113```1114