Введение
В предыдущем посте был введен атрибут свойства. Также было упомянуто, что он управляется механизмом низкого уровня, называемым дескриптор атрибута.
В этом посте мы продолжим наше путешествие, чтобы лучше понять дескрипторы атрибутов как часть более общего мира атрибутов Python.
Все примеры почтовых индексов были написаны и протестированы на cpython 3.6
Дескриптор атрибута
Оказывается, при получении / установке / удалении атрибута x Python сначала проверяет, является ли значение x дескриптором атрибута. Дескриптор атрибута - это объект, реализующий интерфейс, который называется descriptor protocol
.
Этот интерфейс требует, чтобы у объекта был хотя бы один из следующих методов ( также известные как дескрипторные функции):
__get__(self, obj, type_of_obj=None) --> value
__set__(self, obj, value) --> None
__delete__(self, obj) --> None
Если этого не произойдет, мы получим ожидаемое поведение по умолчанию, то есть при доступе к «x» будет получен доступ только к объекту, не являющемуся дескриптором (обычно путем доступа к obj или type (obj) __dict__).
Однако, если он реализует один из вышеперечисленных методов, python соблюдает протокол дескриптора и вызывает функции дескриптора «x» в соответствии с соответствующим режимом доступа.
Например, если класс «Cls» имеет атрибут «x», который реализует интерфейс дескриптора __get__
, тогда obj.x
вернет x.__get__(obj, type(obj))
Знакомо?
Конечно!
Это очень похоже на атрибут свойства, который вызывает свою собственную функцию fget, когда мы обращаемся к нему через obj.x.
Свойство - это частный случай дескриптора
Действительно, функция property () на самом деле является построителем дескрипторов высокого уровня, который был красиво и элегантно разработан как функция «все в одном», которая с одной стороны служит фабрикой дескрипторов и, с другой стороны, набор декораторов, помогающий преобразовывать обычные методы в функции-дескрипторы.
Давайте докажем это, посмотрев, реализует ли свойство протокол дескриптора.
Более того, официальный python Descriptor HowTo Guide предоставляет код, имитирующий, как в основном реализована функция property () в C.
приведенный выше фрагмент хорошо демонстрирует, как fget, fset и fdel обертываются соответствующими функциями дескриптора __get__, __set__ и __delete__.
Здесь также показано, как методы свойств служат декораторами, которые принимают функции класса, которые обычно становятся методами обычных объектов, и «внедряют» их. как fget / fset / fdel свойства.
Хотя атрибут свойства является частным случаем атрибута дескриптора, последний имеет более общее поведение в отношении доступа и вызова функции.
Доступ к атрибутам дескриптора
Доступ к атрибутам дескриптора является частью полного механизма доступа к атрибутам Python, который включает логику, называемую цепочкой приоритета доступа к атрибутам. Эта логика зависит от таких факторов:
- Переопределяет ли класс функцию доступа.
- Является ли атрибут атрибутом экземпляра или атрибутом класса.
- Является ли атрибут объектом дескриптора.
- Если дескриптор, то какую часть протокола дескриптора он реализует.
Чтобы понять, как работает доступ к дескрипторам, давайте разделим его на две части:
- Как вызывается дескриптор, если логика решает это сделать.
- Как логика решает, вызывать ли дескрипторные функции.
Как вызывается дескриптор, если логика решает это сделать
Предположим, что у нас есть класс Cls с атрибутом класса «x», который имеет тип (класс), реализующий функции __get__, __ set__ и __delete__. Давайте также определим obj как экземпляр этого Cls, так что Cls на самом деле является type (obj).
Примечание. Для простоты предполагается, что атрибут «x» определен в Cls, поэтому находится в Cls .__ dict__. Однако в более общем случае он может быть унаследован (как и любой атрибут), поэтому «x» фактически ищется в Cls или одном из его предков (за исключением метаклассов). Этот поиск выполняется с помощью обычного механизма __mro__ (порядок разрешения методов).
Ниже объясняется, что происходит под капотом, когда мы обращаемся к дескриптору «x» из obj и из Cls.
** How the attribute descriptor are invoked when access logic decides to do so.** Get Access: obj.x => Cls.__dict__[‘x’].__get__(obj, Cls) Cls.x => Cls.__dict__[‘x’].__get__(None, Cls) Set Access: obj.x = v => Cls.__dict__[‘x’].__set__(obj, val) Cls.x = v => Regular override x class attribute with v Delete Access: del obj.x => Cls.__dict__[‘x’].__delete__(obj) del Cls.x => Regular deletion of x from the class Cls.
Таким образом, доступ к obj.x фактически вызывает методы атрибута класса x, которые получают obj в качестве аргумента, таким образом косвенно имея полный контроль над obj, хотя obj не является владельцем x. Мы уже обсуждали это относительно атрибута свойства и отношения obj в прошлом посте.
Как логика решает, вызывать ли функции дескриптора
Чтобы понять это, нам сначала нужно понять цепочку приоритета доступа к атрибутам.
В волновой передаче цепочка приоритетов:
получить:
- __getattribute__
- дескриптор данных
- атрибут экземпляра
- дескриптор, не связанный с данными
- атрибут класса
- __getattr__
- AttributeError
set / del:
- __setattr__ / __delattr__
- дескриптор данных
- атрибут экземпляра
- AttributeError
Вы видите, что здесь есть два новых термина: дескриптор данных и дескриптор данных, которые влияют на порядок доступа.
Типы дескрипторов атрибутов
Python классифицирует дескриптор на два типа:
- Дескрипторы, не относящиеся к данным - дескрипторы, имеющие только __get__
- Дескрипторы данных - дескрипторы, содержащие __set__ и / или __delete__.
Дескриптор данных определен в исходном коде Include / descrobject.h.
Include/descrobject.h #define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)
Когда tp_descr_set является слотом типа (класса) для функции __set__ / __delete__. PyDescr_IsData имеет значение True, если tp_descr_set не равно NULL, что означает, что класс определяет __set__ и / или __delete__.
Важно отметить, что хотя на уровне python __set__ и __delete__ являются независимыми функциями, на базовом уровне c удаление - это просто особый случай установки атрибута с NULL значение.
Примечание. Код C здесь главным образом потому, что официальная документация Python немного расплывчата с определением терминов. В то время как официальная Модель данных согласуется с реализацией исходного кода (здесь), Руководство по дескрипторам странным образом определяет ее по-другому (здесь).
Доступ к дескрипторам в действии
Теперь давайте посмотрим, как работает дескрипторный доступ как часть общего механизма доступа. Ниже представлены:
- Блок-схемы, демонстрирующие полную цепочку приоритетов для obj.x.
- Полный охват всех вариантов использования obj.x и Cls.x, когда __getattribute__ / __setattr__ и __delattr__ не переопределены.
- В конце концов, есть мощный фрагмент кода, демонстрирующий примеры всех вариантов использования.
Примечание. В приведенном ниже примере предполагается, что Cls имеет метакласс по умолчанию (или, по крайней мере, тот, у которого нет атрибутов дескриптора, которые могут повлиять на доступ к Cls.x).
Атрибут Get in Action
Примечания:
- В приведенных ниже сценариях использования предполагается, что __getattribute__ не переопределен (в противном случае все перечисленные ниже не будут вызваны в первую очередь).
- Если приведенные ниже действия get завершаются ошибкой AttributeError, вызывается Cls .__ getattr__ (если определено).
Дескриптор данных - получение доступа к объекту (obj.x):
- Если дескриптор данных имеет __get__, Вызовите его
Cls .__ dict __ ['x'] .__ get __ (obj, Cls )ex. 1 in code
- Если в дескрипторе данных нет __get__ И
есть атрибут экземпляра с таким же именем, откройте его.
obj .__ dict __ [ 'x']ex. 2 in code
- В противном случае верните сам экземпляр дескриптора (это всегда допустимый вариант)
Cls .__ dict __ [‘x’]ex. 3 in code
Дескриптор, не связанный с данными - обращение к объекту для получения доступа (obj.x):
- Если существует атрибут экземпляра с тем же именем, что и дескриптор, не связанный с данными, откройте его.
obj .__ dict __ [‘x’]ex. 4 in code
- В противном случае вызовите __get__ дескриптора, не связанного с данными (по определению имеет __get__)
Cls .__ dict __ ['x' ] .__ get __ (obj, Cls)ex. 5 in code
Дескриптор данных - вызов получения доступа к классу (Cls.x):
- Если x имеет __get__, вызвать его.
Cls .__ dict __ ['x'] .__ get __ (None, Cls )ex. 6 in code
- В противном случае верните сам экземпляр дескриптора (этот параметр всегда действителен)
Cls .__ dict __ [‘x’]ex. 7 in code
Дескриптор, не связанный с данными - Вызов получения доступа к классу (Cls.x):
- Вызвать __get__ дескриптора, не связанного с данными (по определению имеет __get__)
Cls .__ dict __ ['x']. __get __ (Нет, Cls)ex. 8 in code
Если кто-то хочет понять лежащую в основе реализацию c-уровня того, как работает получение доступа, я настоятельно рекомендую отличный пост Сайеда Комайла Аббаса.
Установка / удаление атрибутов в действии
Установить
Дескриптор данных - вызов набора доступа к объекту (obj.x = val)
- Если __set__ существует, вызовите его.
Cls .__ dict __ [‘x’] .__ set __ (obj, val)ex. 9 in code
- Если __set__ не существует (реализован только __delete__, то есть дескриптор данных), вызовите AttributeError
ex. 10 in code
Дескриптор без данных - вызов набора доступа (obj.x = val):
- По определению нет __set__, поэтому установите «x» в экземпляре __dict__ (создать или изменить).
obj. __dict __ ['x'] = valex. 11 in code
Дескриптор без данных / данных - вызов набора доступа к классу (Cls.x = val):
- Обычная настройка атрибута класса.
Cls .__ dict __ [‘x’] = valex. 12 in code
Удалить
Дескриптор данных - обращение к объекту доступа на удаление (del obj.x):
- Если __delete__ существует, вызовите его.
Cls .__ dict __ [‘x’] .__ delete __ (obj)ex. 13 in code
- Иначе, __delete__ не существует (реализован только __set__, поэтому по-прежнему дескриптор данных), поднять AttributeError исключение.
ex. 14 in code
Дескриптор, не связанный с данными - обращение к объекту доступа на удаление (del obj.x):
- Если obj имеет атрибут с таким же именем в своем __dict__,
удалите его как обычное удаление.
del obj .__ dict __ ['x']ex. 15 in code
- В противном случае вызовет исключение AttributeError.
ex. 16 in code
Дескриптор, не связанный с данными / данными - вызов доступа на удаление для класса (del Cls.x)
- Удаление атрибута обычного класса
del Cls .__ dict __ [‘x’]ex. 17 in code
Дескрипторы в действии - примеры кода
Следующий фрагмент демонстрирует поведение дескриптора для каждого типа.
Использование дескрипторов в Python будет темой моего следующего поста.