Документация Python неофициальный перевод

descriptor.md

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

1> **Источник:** https://python-all.ru/3.15/howto/descriptor.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# [Руководство по дескрипторам](https://python-all.ru/3.15/howto/descriptor.html#id1)89Содержание [Руководство по дескрипторам](https://python-all.ru/3.15/howto/descriptor.html#descriptor-guide) [Введение](https://python-all.ru/3.15/howto/descriptor.html#primer) [Простой пример: дескриптор, возвращающий константу](https://python-all.ru/3.15/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant) [Динамический поиск](https://python-all.ru/3.15/howto/descriptor.html#dynamic-lookups) [Управляемые атрибуты](https://python-all.ru/3.15/howto/descriptor.html#managed-attributes) [Настраиваемые имена](https://python-all.ru/3.15/howto/descriptor.html#customized-names) [Заключительные мысли](https://python-all.ru/3.15/howto/descriptor.html#closing-thoughts) [Полный практический пример](https://python-all.ru/3.15/howto/descriptor.html#complete-practical-example) [Класс-валидатор](https://python-all.ru/3.15/howto/descriptor.html#validator-class) [Пользовательские валидаторы](https://python-all.ru/3.15/howto/descriptor.html#custom-validators) [Практическое применение](https://python-all.ru/3.15/howto/descriptor.html#practical-application) [Техническое руководство](https://python-all.ru/3.15/howto/descriptor.html#technical-tutorial) [Аннотация](https://python-all.ru/3.15/howto/descriptor.html#abstract) [Определение и введение](https://python-all.ru/3.15/howto/descriptor.html#definition-and-introduction) [Протокол дескриптора](https://python-all.ru/3.15/howto/descriptor.html#descriptor-protocol) [Обзор вызова дескриптора](https://python-all.ru/3.15/howto/descriptor.html#overview-of-descriptor-invocation) [Вызов из экземпляра](https://python-all.ru/3.15/howto/descriptor.html#invocation-from-an-instance) [Вызов из класса](https://python-all.ru/3.15/howto/descriptor.html#invocation-from-a-class) [Вызов через super](https://python-all.ru/3.15/howto/descriptor.html#invocation-from-super) [Сводка логики вызова](https://python-all.ru/3.15/howto/descriptor.html#summary-of-invocation-logic) [Автоматическое уведомление об имени](https://python-all.ru/3.15/howto/descriptor.html#automatic-name-notification) [Пример ORM](https://python-all.ru/3.15/howto/descriptor.html#orm-example) [Эквиваленты на чистом Python](https://python-all.ru/3.15/howto/descriptor.html#pure-python-equivalents) [Свойства](https://python-all.ru/3.15/howto/descriptor.html#properties) [Функции и методы](https://python-all.ru/3.15/howto/descriptor.html#functions-and-methods) [Виды методов](https://python-all.ru/3.15/howto/descriptor.html#kinds-of-methods) [Статические методы](https://python-all.ru/3.15/howto/descriptor.html#static-methods) [Методы класса](https://python-all.ru/3.15/howto/descriptor.html#class-methods) [Объекты-члены и \_\_slots\_\_](https://python-all.ru/3.15/howto/descriptor.html#member-objects-and-slots)1011[Дескрипторы](https://python-all.ru/3.15/glossary.html#term-descriptor) позволяют объектам настраивать поиск, хранение и удаление атрибутов.1213В этом руководстве четыре основных раздела:14151. «Введение» даёт базовый обзор, плавно переходя от простых примеров, добавляя по одной возможности за раз. Начните отсюда, если вы новичок в дескрипторах.162. Второй раздел показывает полный практический пример дескриптора. Если вы уже знаете основы, начинайте с него.173. Третий раздел представляет более техническое руководство, которое углубляется в детальные механизмы работы дескрипторов. Большинству людей не нужна такая степень детализации.184. Последний раздел содержит чистые аналоги Python для встроенных дескрипторов, написанных на C. Прочитайте его, если вам интересно, как функции превращаются в связанные методы или как реализованы распространённые инструменты, такие как [`classmethod()`](https://python-all.ru/3.15/library/functions.html#classmethod), [`staticmethod()`](https://python-all.ru/3.15/library/functions.html#staticmethod), [`property()`](https://python-all.ru/3.15/library/functions.html#property) и [\_\_slots\_\_](https://python-all.ru/3.15/glossary.html#term-__slots__).1920## [Введение](https://python-all.ru/3.15/howto/descriptor.html#id2)2122В этом введении мы начнём с самого простого возможного примера, а затем будем добавлять новые возможности одну за другой.2324### [Простой пример: дескриптор, возвращающий константу](https://python-all.ru/3.15/howto/descriptor.html#id3)2526Класс `Ten` – это дескриптор, у которого метод [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__) всегда возвращает константу `10`:2728```python29class Ten:30    def __get__(self, obj, objtype=None):31        return 1032```3334Чтобы использовать дескриптор, его нужно сохранить как переменную класса в другом классе:3536```python37class A:38    x = 5                       # Обычный атрибут класса39    y = Ten()                   # Экземпляр дескриптора40```4142Интерактивный сеанс показывает разницу между обычным поиском атрибута и поиском через дескриптор:4344```pycon45>>> a = A()                     # Создать экземпляр класса A46>>> a.x                         # Обычный поиск атрибута47548>>> a.y                         # Поиск через дескриптор491050```5152При поиске атрибута `a.x` оператор точки находит `'x': 5` в словаре класса. При поиске `a.y` оператор точки находит экземпляр дескриптора, распознаваемый по его методу `__get__`. Вызов этого метода возвращает `10`.5354Обратите внимание, что значение `10` не хранится ни в словаре класса, ни в словаре экземпляра. Вместо этого значение `10` вычисляется по требованию.5556Этот пример показывает, как работает простой дескриптор, но он не очень полезен. Для получения констант лучше подойдёт обычный поиск атрибута.5758В следующем разделе мы создадим нечто более полезное – динамический поиск.5960### [Динамический поиск](https://python-all.ru/3.15/howto/descriptor.html#id4)6162Интересные дескрипторы обычно выполняют вычисления вместо возврата констант:6364```python65import os6667class DirectorySize:6869    def __get__(self, obj, objtype=None):70        return len(os.listdir(obj.dirname))7172class Directory:7374    size = DirectorySize()              # Экземпляр дескриптора7576    def __init__(self, dirname):77        self.dirname = dirname          # Обычный атрибут экземпляра78```7980Интерактивный сеанс показывает, что поиск динамический – каждый раз вычисляются разные, обновлённые ответы:8182```python83>>> s = Directory('songs')84>>> g = Directory('games')85>>> s.size                              # В каталоге songs двадцать файлов862087>>> g.size                              # В каталоге games три файла88389>>> os.remove('games/chess')            # Удалить игру90>>> g.size                              # Счётчик файлов обновляется автоматически91292```9394Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример также раскрывает назначение параметров [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__). Параметр *self* – это *size*, экземпляр *DirectorySize*. Параметр *obj* – это либо *g*, либо *s*, экземпляр *Directory*. Именно параметр *obj* позволяет методу `__get__()` узнать целевой каталог. Параметр *objtype* – это класс *Directory*.9596### [Управляемые атрибуты](https://python-all.ru/3.15/howto/descriptor.html#id5)9798Популярное применение дескрипторов – управление доступом к данным экземпляра. Дескриптор назначается публичному атрибуту в словаре класса, в то время как фактические данные хранятся как приватный атрибут в словаре экземпляра. Методы дескриптора [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__) и [`__set__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set__) вызываются при обращении к публичному атрибуту.99100В следующем примере *age* – публичный атрибут, а *\_age* – приватный атрибут. При обращении к публичному атрибуту дескриптор регистрирует поиск или обновление:101102```python103import logging104105logging.basicConfig(level=logging.INFO)106107class LoggedAgeAccess:108109    def __get__(self, obj, objtype=None):110        value = obj._age111        logging.info('Accessing %r giving %r', 'age', value)112        return value113114    def __set__(self, obj, value):115        logging.info('Updating %r to %r', 'age', value)116        obj._age = value117118class Person:119120    age = LoggedAgeAccess()             # Экземпляр дескриптора121122    def __init__(self, name, age):123        self.name = name                # Обычный атрибут экземпляра124        self.age = age                  # Вызывает __set__()125126    def birthday(self):127        self.age += 1                   # Вызывает и __get__(), и __set__()128```129130Интерактивный сеанс показывает, что весь доступ к управляемому атрибуту *age* регистрируется, а обычный атрибут *name* – нет:131132```pycon133>>> mary = Person('Mary M', 30)         # Первоначальное обновление возраста записывается134INFO:root:Updating 'age' to 30135>>> dave = Person('David D', 40)136INFO:root:Updating 'age' to 40137138>>> vars(mary)                          # Фактические данные находятся в приватном атрибуте139{'name': 'Mary M', '_age': 30}140>>> vars(dave)141{'name': 'David D', '_age': 40}142143>>> mary.age                            # Получить доступ к данным и записать поиск144INFO:root:Accessing 'age' giving 3014530146>>> mary.birthday()                     # Обновления также записываются147INFO:root:Accessing 'age' giving 30148INFO:root:Updating 'age' to 31149150>>> dave.name                           # Обычный поиск атрибута не записывается151'David D'152>>> dave.age                            # Записывается только управляемый атрибут153INFO:root:Accessing 'age' giving 4015440155```156157Одна из главных проблем этого примера – приватное имя *\_age* жёстко задано в классе *LoggedAgeAccess*. Это означает, что каждый экземпляр может иметь только один регистрируемый атрибут, и его имя нельзя изменить. В следующем примере мы исправим эту проблему.158159### [Настраиваемые имена](https://python-all.ru/3.15/howto/descriptor.html#id6)160161Когда класс использует дескрипторы, он может сообщить каждому дескриптору, какое имя переменной было использовано.162163В этом примере класс `Person` имеет два экземпляра дескриптора: *name* и *age*. Когда класс `Person` определён, он делает обратный вызов [`__set_name__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set_name__) в *LoggedAccess*, чтобы имена полей были записаны, давая каждому дескриптору свои *public\_name* и *private\_name*:164165```python166import logging167168logging.basicConfig(level=logging.INFO)169170class LoggedAccess:171172    def __set_name__(self, owner, name):173        self.public_name = name174        self.private_name = '_' + name175176    def __get__(self, obj, objtype=None):177        value = getattr(obj, self.private_name)178        logging.info('Accessing %r giving %r', self.public_name, value)179        return value180181    def __set__(self, obj, value):182        logging.info('Updating %r to %r', self.public_name, value)183        setattr(obj, self.private_name, value)184185class Person:186187    name = LoggedAccess()                # Первый экземпляр дескриптора188    age = LoggedAccess()                 # Второй экземпляр дескриптора189190    def __init__(self, name, age):191        self.name = name                 # Вызывает первый дескриптор192        self.age = age                   # Вызывает второй дескриптор193194    def birthday(self):195        self.age += 1196```197198Интерактивный сеанс показывает, что класс `Person` вызвал [`__set_name__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set_name__), чтобы имена полей были записаны. Здесь мы вызываем [`vars()`](https://python-all.ru/3.15/library/functions.html#vars) для поиска дескриптора без его активации:199200```pycon201>>> vars(vars(Person)['name'])202{'public_name': 'name', 'private_name': '_name'}203>>> vars(vars(Person)['age'])204{'public_name': 'age', 'private_name': '_age'}205```206207Новый класс теперь регистрирует доступ как к *name*, так и к *age*:208209```pycon210>>> pete = Person('Peter P', 10)211INFO:root:Updating 'name' to 'Peter P'212INFO:root:Updating 'age' to 10213>>> kate = Person('Catherine C', 20)214INFO:root:Updating 'name' to 'Catherine C'215INFO:root:Updating 'age' to 20216```217218Два экземпляра *Person* содержат только приватные имена:219220```pycon221>>> vars(pete)222{'_name': 'Peter P', '_age': 10}223>>> vars(kate)224{'_name': 'Catherine C', '_age': 20}225```226227### [Заключительные мысли](https://python-all.ru/3.15/howto/descriptor.html#id7)228229[Дескриптором](https://python-all.ru/3.15/glossary.html#term-descriptor) мы называем любой объект, который определяет [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set__) или [`__delete__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__delete__).230231Дескрипторы могут дополнительно иметь метод [`__set_name__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set_name__). Он используется только в случаях, когда дескриптору нужно знать либо класс, в котором он был создан, либо имя переменной класса, которой он был присвоен. (Если этот метод присутствует, он вызывается, даже если класс не является дескриптором.)232233Дескрипторы вызываются оператором точки при поиске атрибута. Если дескриптор доступен косвенно через `vars(some_class)[descriptor_name]`, возвращается экземпляр дескриптора без его вызова.234235Дескрипторы работают только при использовании в качестве переменных класса. Если их поместить в экземпляры, они не оказывают никакого эффекта.236237Основная мотивация дескрипторов – предоставить точку входа, позволяющую объектам, хранящимся в переменных класса, контролировать то, что происходит во время поиска атрибута.238239Традиционно вызывающий класс контролирует то, что происходит во время поиска. Дескрипторы инвертируют это отношение и позволяют данным, к которым осуществляется поиск, влиять на процесс.240241Дескрипторы используются повсеместно в языке. Именно так функции превращаются в связанные методы. Распространенные инструменты, такие как [`classmethod()`](https://python-all.ru/3.15/library/functions.html#classmethod), [`staticmethod()`](https://python-all.ru/3.15/library/functions.html#staticmethod), [`property()`](https://python-all.ru/3.15/library/functions.html#property) и [`functools.cached_property()`](https://python-all.ru/3.15/library/functools.html#functools.cached_property), все реализованы как дескрипторы.242243## [Полный практический пример](https://python-all.ru/3.15/howto/descriptor.html#id8)244245В этом примере создается практичный и мощный инструмент для поиска пресловутых труднообнаруживаемых ошибок повреждения данных.246247### [Класс Validator](https://python-all.ru/3.15/howto/descriptor.html#id9)248249Валидатор – это дескриптор для управляемого доступа к атрибутам. Перед сохранением любых данных он проверяет, соответствует ли новое значение различным ограничениям типа и диапазона. Если эти ограничения не соблюдены, вызывается исключение, чтобы предотвратить повреждение данных на раннем этапе.250251Этот класс `Validator` является одновременно [абстрактным базовым классом](https://python-all.ru/3.15/glossary.html#term-abstract-base-class) и дескриптором управляемого атрибута:252253```python254from abc import ABC, abstractmethod255256class Validator(ABC):257258    def __set_name__(self, owner, name):259        self.private_name = '_' + name260261    def __get__(self, obj, objtype=None):262        return getattr(obj, self.private_name)263264    def __set__(self, obj, value):265        self.validate(value)266        setattr(obj, self.private_name, value)267268    @abstractmethod269    def validate(self, value):270        pass271```272273Пользовательские валидаторы должны наследовать от `Validator` и предоставлять метод `validate()` для проверки различных ограничений по мере необходимости.274275### [Пользовательские валидаторы](https://python-all.ru/3.15/howto/descriptor.html#id10)276277Вот три практические утилиты для проверки данных:2782791. `OneOf` проверяет, что значение является одним из ограниченного набора опций.2802. `Number` проверяет, что значение является либо [`int`](https://python-all.ru/3.15/library/functions.html#int), либо [`float`](https://python-all.ru/3.15/library/functions.html#float). Опционально он проверяет, что значение находится между заданным минимумом или максимумом.2813. `String` проверяет, что значение является [`str`](https://python-all.ru/3.15/library/stdtypes.html#str). Опционально он проверяет заданную минимальную или максимальную длину. Он также может проверять определенный пользователем [предикат](https://python-all.ru/3.15/howto/descriptor.html).282283```python284class OneOf(Validator):285286    def __init__(self, *options):287        self.options = set(options)288289    def validate(self, value):290        if value not in self.options:291            raise ValueError(292                f'Expected {value!r} to be one of {self.options!r}'293            )294295class Number(Validator):296297    def __init__(self, minvalue=None, maxvalue=None):298        self.minvalue = minvalue299        self.maxvalue = maxvalue300301    def validate(self, value):302        if not isinstance(value, (int, float)):303            raise TypeError(f'Expected {value!r} to be an int or float')304        if self.minvalue is not None and value < self.minvalue:305            raise ValueError(306                f'Expected {value!r} to be at least {self.minvalue!r}'307            )308        if self.maxvalue is not None and value > self.maxvalue:309            raise ValueError(310                f'Expected {value!r} to be no more than {self.maxvalue!r}'311            )312313class String(Validator):314315    def __init__(self, minsize=None, maxsize=None, predicate=None):316        self.minsize = minsize317        self.maxsize = maxsize318        self.predicate = predicate319320    def validate(self, value):321        if not isinstance(value, str):322            raise TypeError(f'Expected {value!r} to be a str')323        if self.minsize is not None and len(value) < self.minsize:324            raise ValueError(325                f'Expected {value!r} to be no smaller than {self.minsize!r}'326            )327        if self.maxsize is not None and len(value) > self.maxsize:328            raise ValueError(329                f'Expected {value!r} to be no bigger than {self.maxsize!r}'330            )331        if self.predicate is not None and not self.predicate(value):332            raise ValueError(333                f'Expected {self.predicate} to be true for {value!r}'334            )335```336337### [Практическое применение](https://python-all.ru/3.15/howto/descriptor.html#id11)338339Вот как валидаторы данных могут быть использованы в реальном классе:340341```python342class Component:343344    name = String(minsize=3, maxsize=10, predicate=str.isupper)345    kind = OneOf('wood', 'metal', 'plastic')346    quantity = Number(minvalue=0)347348    def __init__(self, name, kind, quantity):349        self.name = name350        self.kind = kind351        self.quantity = quantity352```353354Дескрипторы предотвращают создание недопустимых экземпляров:355356```pycon357>>> Component('Widget', 'metal', 5)      # Заблокировано: 'Widget' не в верхнем регистре358Traceback (most recent call last):359    ...360ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'361362>>> Component('WIDGET', 'metle', 5)      # Заблокировано: 'metle' написано с ошибкой363Traceback (most recent call last):364    ...365ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}366367>>> Component('WIDGET', 'metal', -5)     # Заблокировано: -5 отрицательное368Traceback (most recent call last):369    ...370ValueError: Expected -5 to be at least 0371372>>> Component('WIDGET', 'metal', 'V')    # Заблокировано: 'V' не является числом373Traceback (most recent call last):374    ...375TypeError: Expected 'V' to be an int or float376377>>> c = Component('WIDGET', 'metal', 5)  # Разрешено: входные данные корректны378```379380## [Техническое руководство](https://python-all.ru/3.15/howto/descriptor.html#id12)381382Далее следует более техническое руководство по механике и деталям того, как работают дескрипторы.383384### [Аннотация](https://python-all.ru/3.15/howto/descriptor.html#id13)385386Определяет дескрипторы, обобщает протокол и показывает, как дескрипторы вызываются. Приводится пример, показывающий, как работают объектно-реляционные отображения.387388Изучение дескрипторов не только открывает доступ к более широкому набору инструментов, но и дает более глубокое понимание того, как работает Python.389390### [Определение и введение](https://python-all.ru/3.15/howto/descriptor.html#id14)391392В общем случае дескриптор – это значение атрибута, которое имеет один из методов протокола дескриптора. Эти методы: [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), [`__set__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set__), и [`__delete__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__delete__). Если для атрибута определен хотя бы один из этих методов, то такой атрибут называется [дескриптором](https://python-all.ru/3.15/glossary.html#term-descriptor).393394Поведение по умолчанию при доступе к атрибуту – получить, установить или удалить атрибут из словаря объекта. Например, `a.x` имеет цепочку поиска, начиная с `a.__dict__['x']`, затем `type(a).__dict__['x']` и продолжая порядком разрешения методов `type(a)`. Если найденное значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. Где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены.395396Дескрипторы – это мощный протокол общего назначения. Они являются механизмом, лежащим в основе свойств, методов, статических методов, методов класса и [`super()`](https://python-all.ru/3.15/library/functions.html#super). Они используются во всем Python. Дескрипторы упрощают базовый код на C и предоставляют гибкий набор новых инструментов для повседневных программ на Python.397398### [Протокол дескриптора](https://python-all.ru/3.15/howto/descriptor.html#id15)399400`descr.__get__(self, obj, type=None)`401402`descr.__set__(self, obj, value)`403404`descr.__delete__(self, obj)`405406Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.407408Если объект определяет [`__set__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set__) или [`__delete__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__delete__), он считается дескриптором данных. Дескрипторы, которые определяют только [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), называются дескрипторами, не являющимися дескрипторами данных (они часто используются для методов, но возможно и другое применение).409410Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.411412Чтобы создать дескриптор данных только для чтения, определите и [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), и [`__set__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set__), причём `__set__()` должен возбуждать [`AttributeError`](https://python-all.ru/3.15/library/exceptions.html#AttributeError) при вызове. Определения метода `__set__()` с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.413414### [Обзор вызова дескрипторов](https://python-all.ru/3.15/howto/descriptor.html#id16)415416Дескриптор можно вызвать напрямую с помощью `desc.__get__(obj)` или `desc.__get__(None, cls)`.417418Но чаще дескриптор вызывается автоматически при доступе к атрибуту.419420Выражение `obj.x` выполняет поиск атрибута `x` в цепочке пространств имён для `obj`. Если поиск находит дескриптор вне [`__dict__`](https://python-all.ru/3.15/reference/datamodel.html#object.__dict__) экземпляра, вызывается его метод [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__) в соответствии с правилами приоритета, перечисленными ниже.421422Детали вызова зависят от того, является ли `obj` объектом, классом или экземпляром super.423424### [Вызов из экземпляра](https://python-all.ru/3.15/howto/descriptor.html#id17)425426Поиск в экземпляре просматривает цепочку пространств имён, отдавая наивысший приоритет дескрипторам данных, затем переменным экземпляра, затем дескрипторам не-данных, затем переменным класса и, наконец, [`__getattr__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattr__), если он предусмотрен.427428Если дескриптор найден для `a.x`, он вызывается с: `desc.__get__(a, type(a))`.429430Логика точечного поиска находится в [`object.__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__). Вот эквивалент на чистом Python:431432```python433def find_name_in_mro(cls, name, default):434    "Emulate _PyType_Lookup() in Objects/typeobject.c"435    for base in cls.__mro__:436        if name in vars(base):437            return vars(base)[name]438    return default439440def object_getattribute(obj, name):441    "Emulate PyObject_GenericGetAttr() in Objects/object.c"442    null = sentinel('null')443    objtype = type(obj)444    cls_var = find_name_in_mro(objtype, name, null)445    descr_get = getattr(type(cls_var), '__get__', null)446    if descr_get is not null:447        if (hasattr(type(cls_var), '__set__')448            or hasattr(type(cls_var), '__delete__')):449            return descr_get(cls_var, obj, objtype)     # дескриптор данных450    if hasattr(obj, '__dict__') and name in vars(obj):451        return vars(obj)[name]                          # переменная экземпляра452    if descr_get is not null:453        return descr_get(cls_var, obj, objtype)         # дескриптор без данных454    if cls_var is not null:455        return cls_var                                  # переменная класса456    raise AttributeError(name)457```458459Обратите внимание: в коде [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__) нет ловушки [`__getattr__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattr__). Именно поэтому прямой вызов `__getattribute__()` или вызов с `super().__getattribute__` полностью обходит `__getattr__()`.460461Вместо этого именно оператор точки и функция [`getattr()`](https://python-all.ru/3.15/library/functions.html#getattr) отвечают за вызов [`__getattr__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattr__) всякий раз, когда [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__) возбуждает [`AttributeError`](https://python-all.ru/3.15/library/exceptions.html#AttributeError). Их логика инкапсулирована во вспомогательной функции:462463```python464def getattr_hook(obj, name):465    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"466    try:467        return obj.__getattribute__(name)468    except AttributeError:469        if not hasattr(type(obj), '__getattr__'):470            raise471    return type(obj).__getattr__(obj, name)             # getattr472```473474### [Вызов из класса](https://python-all.ru/3.15/howto/descriptor.html#id18)475476Логика точечного поиска, такого как `A.x`, находится в `type.__getattribute__()`. Шаги аналогичны шагам для `object.__getattribute__()`, но поиск по словарю экземпляра заменяется поиском по [порядку разрешения методов](https://python-all.ru/3.15/glossary.html#term-method-resolution-order) класса.477478Если дескриптор найден, он вызывается с `desc.__get__(None, A)`.479480Полную реализацию на C можно найти в `type_getattro()` и `_PyType_Lookup()` в [Objects/typeobject.c](https://python-all.ru/src/3.15/Objects/typeobject.c).481482### [Вызов из super](https://python-all.ru/3.15/howto/descriptor.html#id19)483484Логика точечного поиска в super находится в методе [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__) объекта, возвращаемого [`super()`](https://python-all.ru/3.15/library/functions.html#super).485486Точечный поиск, такой как `super(A, obj).m`, ищет в `obj.__class__.__mro__` базовый класс `B`, следующий сразу за `A`, и затем возвращает `B.__dict__['m'].__get__(obj, A)`. Если это не дескриптор, `m` возвращается без изменений.487488Полную реализацию на C можно найти в `super_getattro()` в [Objects/typeobject.c](https://python-all.ru/src/3.15/Objects/typeobject.c). Эквивалент на чистом Python можно найти в [руководстве Гвидо](https://python-all.ru/3.15/howto/descriptor.html).489490### [Сводка по логике вызова](https://python-all.ru/3.15/howto/descriptor.html#id20)491492Механизм дескрипторов встроен в методы [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__) для [`object`](https://python-all.ru/3.15/library/functions.html#object), [`type`](https://python-all.ru/3.15/library/functions.html#type) и [`super()`](https://python-all.ru/3.15/library/functions.html#super).493494Важно запомнить следующее:495496- Дескрипторы вызываются методом [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__).497- Классы наследуют этот механизм от [`object`](https://python-all.ru/3.15/library/functions.html#object), [`type`](https://python-all.ru/3.15/library/functions.html#type) или [`super()`](https://python-all.ru/3.15/library/functions.html#super).498- Переопределение [`__getattribute__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__getattribute__) предотвращает автоматические вызовы дескрипторов, поскольку вся логика дескрипторов находится в этом методе.499- `object.__getattribute__()` и `type.__getattribute__()` по-разному вызывают [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__). Первый включает экземпляр и может включать класс. Второй передаёт `None` в качестве экземпляра и всегда включает класс.500- Дескрипторы данных всегда переопределяют словари экземпляров.501- Дескрипторы не-данных могут быть переопределены словарями экземпляров.502503### [Автоматическое уведомление об имени](https://python-all.ru/3.15/howto/descriptor.html#id21)504505Иногда желательно, чтобы дескриптор знал имя переменной класса, которой он был присвоен. Когда создаётся новый класс, метакласс [`type`](https://python-all.ru/3.15/library/functions.html#type) сканирует словарь нового класса. Если какие-либо записи являются дескрипторами и в них определён [`__set_name__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set_name__), этот метод вызывается с двумя аргументами. *Владелец* – это класс, в котором используется дескриптор, а *имя* – это переменная класса, которой был присвоен дескриптор.506507Детали реализации находятся в `type_new()` и `set_names()` в [Objects/typeobject.c](https://python-all.ru/src/3.15/Objects/typeobject.c).508509Поскольку логика обновления находится в `type.__new__()`, уведомления происходят только в момент создания класса. Если дескрипторы добавляются в класс после этого, [`__set_name__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__set_name__) потребуется вызвать вручную.510511### [Пример ORM](https://python-all.ru/3.15/howto/descriptor.html#id22)512513Следующий код – это упрощённый каркас, показывающий, как дескрипторы данных можно использовать для реализации [объектно-реляционного отображения (ORM)](https://python-all.ru/3.15/howto/descriptor.html).514515Основная идея в том, что данные хранятся во внешней базе данных. Экземпляры Python хранят только ключи к таблицам базы. Дескрипторы берут на себя поиск или обновление:516517```python518class Field:519520    def __set_name__(self, owner, name):521        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'522        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'523524    def __get__(self, obj, objtype=None):525        return conn.execute(self.fetch, [obj.key]).fetchone()[0]526527    def __set__(self, obj, value):528        conn.execute(self.store, [value, obj.key])529        conn.commit()530```531532Можно использовать класс `Field` для определения [моделей](https://python-all.ru/3.15/howto/descriptor.html), описывающих схему для каждой таблицы в базе данных:533534```python535class Movie:536    table = 'Movies'                    # Имя таблицы537    key = 'title'                       # Первичный ключ538    director = Field()539    year = Field()540541    def __init__(self, key):542        self.key = key543544class Song:545    table = 'Music'546    key = 'title'547    artist = Field()548    year = Field()549    genre = Field()550551    def __init__(self, key):552        self.key = key553```554555Чтобы использовать модели, сначала нужно подключиться к базе данных:556557```python558>>> import sqlite3559>>> conn = sqlite3.connect('entertainment.db')560```561562Интерактивный сеанс показывает, как данные извлекаются из базы данных и как их можно обновить:563564```pycon565>>> Movie('Star Wars').director566'George Lucas'567>>> jaws = Movie('Jaws')568>>> f'Released in {jaws.year} by {jaws.director}'569'Released in 1975 by Steven Spielberg'570571>>> Song('Country Roads').artist572'John Denver'573574>>> Movie('Star Wars').director = 'J.J. Abrams'575>>> Movie('Star Wars').director576'J.J. Abrams'577```578579## [Эквиваленты на чистом Python](https://python-all.ru/3.15/howto/descriptor.html#id23)580581Протокол дескрипторов прост и открывает захватывающие возможности. Несколько вариантов использования настолько распространены, что они уже встроены в стандартные инструменты. Свойства (properties), связанные методы, статические методы, методы класса и \_\_slots\_\_ – все они основаны на протоколе дескрипторов.582583### [Свойства](https://python-all.ru/3.15/howto/descriptor.html#id24)584585Вызов [`property()`](https://python-all.ru/3.15/library/functions.html#property) – это краткий способ создания дескриптора данных, который вызывает функцию при доступе к атрибуту. Его сигнатура:586587```python588property(fget=None, fset=None, fdel=None, doc=None) -> property589```590591В документации показан типичный пример использования для определения управляемого атрибута `x`:592593```python594class C:595    def getx(self): return self.__x596    def setx(self, value): self.__x = value597    def delx(self): del self.__x598    x = property(getx, setx, delx, "I'm the 'x' property.")599```600601Чтобы увидеть, как [`property()`](https://python-all.ru/3.15/library/functions.html#property) реализован на уровне протокола дескрипторов, вот эквивалент на чистом Python, реализующий большую часть основной функциональности:602603```python604class Property:605    "Emulate PyProperty_Type() in Objects/descrobject.c"606607    def __init__(self, fget=None, fset=None, fdel=None, doc=None):608        self.fget = fget609        self.fset = fset610        self.fdel = fdel611        if doc is None and fget is not None:612            doc = fget.__doc__613        self.__doc__ = doc614615    def __set_name__(self, owner, name):616        self.__name__ = name617618    def __get__(self, obj, objtype=None):619        if obj is None:620            return self621        if self.fget is None:622            raise AttributeError623        return self.fget(obj)624625    def __set__(self, obj, value):626        if self.fset is None:627            raise AttributeError628        self.fset(obj, value)629630    def __delete__(self, obj):631        if self.fdel is None:632            raise AttributeError633        self.fdel(obj)634635    def getter(self, fget):636        return type(self)(fget, self.fset, self.fdel, self.__doc__)637638    def setter(self, fset):639        return type(self)(self.fget, fset, self.fdel, self.__doc__)640641    def deleter(self, fdel):642        return type(self)(self.fget, self.fset, fdel, self.__doc__)643```644645Встроенная функция [`property()`](https://python-all.ru/3.15/library/functions.html#property) помогает, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения требуют вмешательства метода.646647Например, класс электронной таблицы может предоставлять доступ к значению ячейки через `Cell('b10').value`. Последующие улучшения программы требуют пересчёта ячейки при каждом доступе; однако программист не хочет влиять на существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту значения в дескриптор данных свойства:648649```python650class Cell:651    ...652653    @property654    def value(self):655        "Recalculate the cell before returning value"656        self.recalc()657        return self._value658```659660В этом примере подойдёт как встроенная [`property()`](https://python-all.ru/3.15/library/functions.html#property), так и наш эквивалент `Property()`.661662### [Функции и методы](https://python-all.ru/3.15/howto/descriptor.html#id25)663664Объектно-ориентированные возможности Python построены на функциональной основе. С помощью дескрипторов, не являющихся дескрипторами данных, эти две парадигмы объединяются бесшовно.665666Функции, хранящиеся в словарях классов, превращаются в методы при вызове. Методы отличаются от обычных функций только тем, что экземпляр объекта добавляется перед остальными аргументами. По соглашению экземпляр называется *self*, но может называться *this* или любым другим именем переменной.667668Методы можно создавать вручную с помощью [`types.MethodType`](https://python-all.ru/3.15/library/types.html#types.MethodType), что примерно эквивалентно:669670```python671class MethodType:672    "Emulate PyMethod_Type in Objects/classobject.c"673674    def __init__(self, func, obj):675        self.__func__ = func676        self.__self__ = obj677678    def __call__(self, *args, **kwargs):679        func = self.__func__680        obj = self.__self__681        return func(obj, *args, **kwargs)682683    def __getattribute__(self, name):684        "Emulate method_getset() in Objects/classobject.c"685        if name == '__doc__':686            return self.__func__.__doc__687        return object.__getattribute__(self, name)688689    def __getattr__(self, name):690        "Emulate method_getattro() in Objects/classobject.c"691        return getattr(self.__func__, name)692693    def __get__(self, obj, objtype=None):694        "Emulate method_descr_get() in Objects/classobject.c"695        return self696```697698Для поддержки автоматического создания методов функции включают метод [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__) для привязки методов при доступе к атрибуту. Это означает, что функции являются дескрипторами, не являющимися дескрипторами данных, которые возвращают связанные методы при точечном поиске от экземпляра. Вот как это работает:699700```python701class Function:702    ...703704    def __get__(self, obj, objtype=None):705        "Simulate func_descr_get() in Objects/funcobject.c"706        if obj is None:707            return self708        return MethodType(self, obj)709```710711Запуск следующего класса в интерпретаторе показывает, как дескриптор функции работает на практике:712713```python714class D:715    def f(self):716         return self717718class D2:719    pass720```721722Функция имеет атрибут [полное имя](https://python-all.ru/3.15/glossary.html#term-qualified-name) для поддержки интроспекции:723724```pycon725>>> D.f.__qualname__726'D.f'727```728729Доступ к функции через словарь класса не вызывает [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__). Вместо этого просто возвращается исходный объект функции:730731```python732>>> D.__dict__['f']733<function D.f at 0x00C45070>734```735736Точечный доступ от класса вызывает [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), который просто возвращает исходную функцию без изменений:737738```python739>>> D.f740<function D.f at 0x00C45070>741```742743Интересное поведение происходит при точечном доступе от экземпляра. Точечный поиск вызывает [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), который возвращает объект связанного метода:744745```python746>>> d = D()747>>> d.f748<bound method D.f of <__main__.D object at 0x00B18C90>>749```750751Внутренне связанный метод хранит исходную функцию и связанный экземпляр:752753```python754>>> d.f.__func__755<function D.f at 0x00C45070>756757>>> d.f.__self__758<__main__.D object at 0x00B18C90>759```760761Если вы когда-нибудь задавались вопросом, откуда берётся *self* в обычных методах или откуда *cls* в методах класса – вот ответ!762763### [Виды методов](https://python-all.ru/3.15/howto/descriptor.html#id26)764765Дескрипторы, не являющиеся дескрипторами данных, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.766767Повторим: функции имеют метод [`__get__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__get__), чтобы их можно было преобразовать в метод при доступе как атрибуты. Дескриптор, не являющийся дескриптором данных, преобразует вызов `obj.f(*args)` в `f(obj, *args)`. Вызов `cls.f(*args)` становится `f(*args)`.768769Эта таблица обобщает привязку и два наиболее полезных её варианта:770771> | Преобразование | Вызывается от объекта | Вызывается от класса |772> | --- | --- | --- |773> | функция | f(obj, \*args) | f(\*args) |774> | staticmethod | f(\*args) | f(\*args) |775> | classmethod | f(type(obj), \*args) | f(cls, \*args) |776777### [Статические методы](https://python-all.ru/3.15/howto/descriptor.html#id27)778779Статические методы возвращают базовую функцию без изменений. Вызов `c.f` или `C.f` эквивалентен прямому поиску в `object.__getattribute__(c, "f")` или `object.__getattribute__(C, "f")`. В результате функция становится одинаково доступной как из объекта, так и из класса.780781Хорошими кандидатами для статических методов являются те, которые не ссылаются на переменную `self`.782783Например, пакет для статистики может содержать класс-контейнер для экспериментальных данных. Класс предоставляет обычные методы для вычисления среднего, медианы и других описательных статистик, зависящих от данных. Однако могут быть полезные функции, которые концептуально связаны, но не зависят от данных. Например, `erf(x)` – это удобная процедура преобразования, которая встречается в статистической работе, но не зависит напрямую от конкретного набора данных. Она может быть вызвана как от объекта, так и от класса: `s.erf(1.5) --> 0.9332` или `Sample.erf(1.5) --> 0.9332`.784785Поскольку статические методы возвращают базовую функцию без изменений, примеры вызовов ничем не примечательны:786787```python788class E:789    @staticmethod790    def f(x):791        return x * 10792```793794```pycon795>>> E.f(3)79630797>>> E().f(3)79830799```800801Используя протокол дескриптора без данных, чистая версия [`staticmethod()`](https://python-all.ru/3.15/library/functions.html#staticmethod) на Python выглядела бы так:802803```python804import functools805806class StaticMethod:807    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"808809    def __init__(self, f):810        self.f = f811        functools.update_wrapper(self, f)812813    def __get__(self, obj, objtype=None):814        return self.f815816    def __call__(self, *args, **kwds):817        return self.f(*args, **kwds)818819    @property820    def __annotations__(self):821        return self.f.__annotations__822```823824Вызов [`functools.update_wrapper()`](https://python-all.ru/3.15/library/functools.html#functools.update_wrapper) добавляет атрибут `__wrapped__`, который ссылается на базовую функцию. Он также переносит атрибуты, необходимые для того, чтобы обёртка выглядела как обёрнутая функция, включая [`__name__`](https://python-all.ru/3.15/reference/datamodel.html#function.__name__), [`__qualname__`](https://python-all.ru/3.15/reference/datamodel.html#function.__qualname__) и [`__doc__`](https://python-all.ru/3.15/reference/datamodel.html#function.__doc__).825826### [Методы класса](https://python-all.ru/3.15/howto/descriptor.html#id28)827828В отличие от статических методов, методы класса перед вызовом функции добавляют ссылку на класс в начало списка аргументов. Этот формат одинаков как для вызова от объекта, так и от класса:829830```python831class F:832    @classmethod833    def f(cls, x):834        return cls.__name__, x835```836837```pycon838>>> F.f(3)839('F', 3)840>>> F().f(3)841('F', 3)842```843844Такое поведение полезно, когда методу нужна только ссылка на класс и он не зависит от данных, хранящихся в конкретном экземпляре. Одно из применений методов класса – создание альтернативных конструкторов класса. Например, метод класса [`dict.fromkeys()`](https://python-all.ru/3.15/library/stdtypes.html#dict.fromkeys) создаёт новый словарь из списка ключей. Эквивалент на чистом Python выглядит так:845846```python847class Dict(dict):848    @classmethod849    def fromkeys(cls, iterable, value=None):850        "Emulate dict_fromkeys() in Objects/dictobject.c"851        d = cls()852        for key in iterable:853            d[key] = value854        return d855```856857Теперь новый словарь уникальных ключей можно создать так:858859```pycon860>>> d = Dict.fromkeys('abracadabra')861>>> type(d) is Dict862True863>>> d864{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}865```866867Используя протокол дескриптора без данных, чистая версия [`classmethod()`](https://python-all.ru/3.15/library/functions.html#classmethod) на Python выглядела бы так:868869```python870import functools871872class ClassMethod:873    "Emulate PyClassMethod_Type() in Objects/funcobject.c"874875    def __init__(self, f):876        self.f = f877        functools.update_wrapper(self, f)878879    def __get__(self, obj, cls=None):880        if cls is None:881            cls = type(obj)882        return MethodType(self.f, cls)883```884885Вызов [`functools.update_wrapper()`](https://python-all.ru/3.15/library/functools.html#functools.update_wrapper) в `ClassMethod` добавляет атрибут `__wrapped__`, который ссылается на базовую функцию. Он также переносит атрибуты, необходимые для того, чтобы обёртка выглядела как обёрнутая функция: [`__name__`](https://python-all.ru/3.15/reference/datamodel.html#function.__name__), [`__qualname__`](https://python-all.ru/3.15/reference/datamodel.html#function.__qualname__), [`__doc__`](https://python-all.ru/3.15/reference/datamodel.html#function.__doc__) и [`__annotations__`](https://python-all.ru/3.15/reference/datamodel.html#function.__annotations__).886887### [Объекты-члены и \_\_slots\_\_](https://python-all.ru/3.15/howto/descriptor.html#id29)888889Когда класс определяет `__slots__`, он заменяет словари экземпляров массивом фиксированной длины для значений слотов. С точки зрения пользователя это имеет несколько последствий:8908911. Обеспечивает немедленное обнаружение ошибок из-за опечаток в именах атрибутов. Разрешены только имена атрибутов, указанные в `__slots__`:892893```python894class Vehicle:895    __slots__ = ('id_number', 'make', 'model')896```897898```pycon899>>> auto = Vehicle()900>>> auto.id_nubmer = 'VYE483814LQEX'901Traceback (most recent call last):902    ...903AttributeError: 'Vehicle' object has no attribute 'id_nubmer'904```9059062. Помогает создавать неизменяемые объекты, где дескрипторы управляют доступом к приватным атрибутам, хранящимся в `__slots__`:907908```python909class Immutable:910911    __slots__ = ('_dept', '_name')          # Замена словаря экземпляра912913    def __init__(self, dept, name):914        self._dept = dept                   # Сохранение в частный атрибут915        self._name = name                   # Сохранение в частный атрибут916917    @property                               # Дескриптор только для чтения918    def dept(self):919        return self._dept920921    @property922    def name(self):                         # Дескриптор только для чтения923        return self._name924```925926```pycon927>>> mark = Immutable('Botany', 'Mark Watney')928>>> mark.dept929'Botany'930>>> mark.dept = 'Space Pirate'931Traceback (most recent call last):932    ...933AttributeError: property 'dept' of 'Immutable' object has no setter934>>> mark.location = 'Mars'935Traceback (most recent call last):936    ...937AttributeError: 'Immutable' object has no attribute 'location'938```9399403. Экономит память. В 64-битной сборке Linux экземпляр с двумя атрибутами занимает 48 байт с `__slots__` и 152 байта без него. Этот [шаблон «приспособленец»](https://python-all.ru/3.15/howto/descriptor.html), вероятно, имеет значение только при создании большого количества экземпляров.9419424. Увеличивает скорость. Чтение переменных экземпляра происходит на 35% быстрее с `__slots__` (согласно измерениям на Python 3.10 на процессоре Apple M1).9439445. Блокирует инструменты вроде [`functools.cached_property()`](https://python-all.ru/3.15/library/functools.html#functools.cached_property), которым для корректной работы требуется словарь экземпляра:945946```python947from functools import cached_property948949class CP:950    __slots__ = ()                          # Устраняет словарь экземпляра951952    @cached_property                        # Требуется словарь экземпляра953    def pi(self):954        return 4 * sum((-1.0)**n / (2.0*n + 1.0)955                       for n in reversed(range(100_000)))956```957958```pycon959>>> CP().pi960Traceback (most recent call last):961  ...962TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.963```964965Невозможно создать точную замену `__slots__` на чистом Python, поскольку для этого требуется прямой доступ к C-структурам и управление выделением памяти объектов. Однако можно построить достаточно точную симуляцию, где реальная C-структура для слотов эмулируется частным списком `_slotvalues`. Чтение и запись в эту частную структуру управляются дескрипторами членов:966967```python968null = sentinel('null')969970class Member:971972    def __init__(self, name, clsname, offset):973        'Emulate PyMemberDef in Include/descrobject.h'974        # См. также descr_new() в Objects/descrobject.c975        self.name = name976        self.clsname = clsname977        self.offset = offset978979    def __get__(self, obj, objtype=None):980        'Emulate member_get() in Objects/descrobject.c'981        # См. также PyMember_GetOne() в Python/structmember.c982        if obj is None:983            return self984        value = obj._slotvalues[self.offset]985        if value is null:986            raise AttributeError(self.name)987        return value988989    def __set__(self, obj, value):990        'Emulate member_set() in Objects/descrobject.c'991        obj._slotvalues[self.offset] = value992993    def __delete__(self, obj):994        'Emulate member_delete() in Objects/descrobject.c'995        value = obj._slotvalues[self.offset]996        if value is null:997            raise AttributeError(self.name)998        obj._slotvalues[self.offset] = null9991000    def __repr__(self):1001        'Emulate member_repr() in Objects/descrobject.c'1002        return f'<Member {self.name!r} of {self.clsname!r}>'1003```10041005Метод `type.__new__()` отвечает за добавление объектов-членов в переменные класса:10061007```python1008class Type(type):1009    'Simulate how the type metaclass adds member objects for slots'10101011    def __new__(mcls, clsname, bases, mapping, **kwargs):1012        'Emulate type_new() in Objects/typeobject.c'1013        # type_new() вызывает PyTypeReady(), который вызывает add_methods()1014        slot_names = mapping.get('slot_names', [])1015        for offset, name in enumerate(slot_names):1016            mapping[name] = Member(name, clsname, offset)1017        return type.__new__(mcls, clsname, bases, mapping, **kwargs)1018```10191020Метод [`object.__new__()`](https://python-all.ru/3.15/reference/datamodel.html#object.__new__) отвечает за создание экземпляров, которые имеют слоты вместо словаря экземпляра. Вот примерная симуляция на чистом Python:10211022```python1023class Object:1024    'Simulate how object.__new__() allocates memory for __slots__'10251026    def __new__(cls, *args, **kwargs):1027        'Emulate object_new() in Objects/typeobject.c'1028        inst = super().__new__(cls)1029        if hasattr(cls, 'slot_names'):1030            empty_slots = [null] * len(cls.slot_names)1031            object.__setattr__(inst, '_slotvalues', empty_slots)1032        return inst10331034    def __setattr__(self, name, value):1035        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'1036        cls = type(self)1037        if hasattr(cls, 'slot_names') and name not in cls.slot_names:1038            raise AttributeError(1039                f'{cls.__name__!r} object has no attribute {name!r}'1040            )1041        super().__setattr__(name, value)10421043    def __delattr__(self, name):1044        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'1045        cls = type(self)1046        if hasattr(cls, 'slot_names') and name not in cls.slot_names:1047            raise AttributeError(1048                f'{cls.__name__!r} object has no attribute {name!r}'1049            )1050        super().__delattr__(name)1051```10521053Чтобы использовать симуляцию в реальном классе, достаточно унаследоваться от `Object` и установить [метакласс](https://python-all.ru/3.15/glossary.html#term-metaclass) в `Type`:10541055```python1056class H(Object, metaclass=Type):1057    'Instance variables stored in slots'10581059    slot_names = ['x', 'y']10601061    def __init__(self, x, y):1062        self.x = x1063        self.y = y1064```10651066На этом этапе метакласс загрузил объекты-члены для *x* и *y*:10671068```python1069>>> from pprint import pp1070>>> pp(dict(vars(H)))1071{'__module__': '__main__',1072 '__doc__': 'Instance variables stored in slots',1073 'slot_names': ['x', 'y'],1074 '__init__': <function H.__init__ at 0x7fb5d302f9d0>,1075 'x': <Member 'x' of 'H'>,1076 'y': <Member 'y' of 'H'>}1077```10781079При создании экземпляров у них есть список `slot_values`, в котором хранятся атрибуты:10801081```pycon1082>>> h = H(10, 20)1083>>> vars(h)1084{'_slotvalues': [10, 20]}1085>>> h.x = 551086>>> vars(h)1087{'_slotvalues': [55, 20]}1088```10891090Атрибуты с ошибками в написании или не назначенные атрибуты вызовут исключение:10911092```pycon1093>>> h.xz1094Traceback (most recent call last):1095    ...1096AttributeError: 'H' object has no attribute 'xz'1097```1098