Введение

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

В этом посте мы продолжим наше путешествие, чтобы лучше понять дескрипторы атрибутов как часть более общего мира атрибутов 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, который включает логику, называемую цепочкой приоритета доступа к атрибутам. Эта логика зависит от таких факторов:

  • Переопределяет ли класс функцию доступа.
  • Является ли атрибут атрибутом экземпляра или атрибутом класса.
  • Является ли атрибут объектом дескриптора.
  • Если дескриптор, то какую часть протокола дескриптора он реализует.

Чтобы понять, как работает доступ к дескрипторам, давайте разделим его на две части:

  1. Как вызывается дескриптор, если логика решает это сделать.
  2. Как логика решает, вызывать ли дескрипторные функции.

Как вызывается дескриптор, если логика решает это сделать

Предположим, что у нас есть класс Cls с атрибутом класса «, который имеет тип (класс), реализующий функции __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 классифицирует дескриптор на два типа:

  1. Дескрипторы, не относящиеся к данным - дескрипторы, имеющие только __get__
  2. Дескрипторы данных - дескрипторы, содержащие __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 немного расплывчата с определением терминов. В то время как официальная Модель данных согласуется с реализацией исходного кода (здесь), Руководство по дескрипторам странным образом определяет ее по-другому (здесь).

Доступ к дескрипторам в действии

Теперь давайте посмотрим, как работает дескрипторный доступ как часть общего механизма доступа. Ниже представлены:

  1. Блок-схемы, демонстрирующие полную цепочку приоритетов для obj.x.
  2. Полный охват всех вариантов использования obj.x и Cls.x, когда __getattribute__ / __setattr__ и __delattr__ не переопределены.
  3. В конце концов, есть мощный фрагмент кода, демонстрирующий примеры всех вариантов использования.

Примечание. В приведенном ниже примере предполагается, что 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'] = val ex. 11 in code

Дескриптор без данных / данных - вызов набора доступа к классу (Cls.x = val):

  • Обычная настройка атрибута класса.
    Cls .__ dict __ [‘x’] = val ex. 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 будет темой моего следующего поста.