Что такое дескриптор в программировании
Перейти к содержимому

Что такое дескриптор в программировании

Общие сведения о дескрипторах

Дескрипторы создаются вызовами API и идентифицируют ресурсы.

Дескрипторные данные

Дескриптор — это относительно небольшой блок данных, который полностью описывает объект для GPU в непрозрачном формате, относящееся к GPU. Существует несколько различных типов дескрипторов: представления целевых объектов отрисовки (RTV), представления трафаретов глубины (DSV), представления ресурсов шейдера (SRV), неупорядоченные представления доступа (UAV), представления буферов констант (CBV) и образцы.

Дескрипторы различаются в зависимости от оборудования GPU. Вы можете запросить размер SRV, UAV или CBV, вызвав ID3D12Device::GetDescriptorHandleIncrementSize. Дескрипторы отображаются в этой документации как невидимые единицы; Вот пример.

srv, cbv, uav, and sampler

Дескрипторы создаются вызовами API и включают такие сведения, как ресурс и mip-карты, которые требуется содержать дескриптор.

Драйвер не отслеживает или не содержит ссылки на дескрипторы, оно предназначено для приложения, чтобы убедиться, что используется правильный тип дескриптора и что информация является текущей. Существует одно небольшое исключение из этого; драйвер проверяет целевые привязки отрисовки, чтобы обеспечить правильную работу цепочек буферов.

Дескрипторы объектов не обязательно освобождаются или освобождаются. Драйверы не присоединяют выделения к созданию дескриптора. Однако дескриптор может кодировать ссылки на другие выделения, для которых приложение владеет временем существования. Например, дескриптор для SRV должен содержать виртуальный адрес ресурса D3D (например, текстуры), на который ссылается SRV. Приложение обязано убедиться, что он не использует дескриптор SRV, если базовый ресурс D3D, от него зависит, был уничтожен или изменен (например, объявлен как неотслеживаемый).

Основным способом использования дескрипторов является размещение их в кучах дескриптора, которые резервируют память для дескрипторов.

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

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

ДескрипторЫ ЦП предназначены для немедленного использования, например для копирования, где необходимо определить источник и назначение. Сразу после использования (например, вызов ID3D12GraphicsCommandList::OMSetRenderTargets), их можно повторно использовать или удалить базовую кучу.

Дескрипторы GPU не предназначены для немедленного использования— они определяют расположения из списка команд для использования во время выполнения GPU. Они должны сохраняться до тех пор, пока не будут выполнены все списки команд, ссылающиеся на них.

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

Эти методы возвращают следующие структуры:

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

Безопасно смещать начальное расположение с рядом приращений, копировать дескрипторы и передавать дескрипторы в вызовы API. Небезопасно разыменовывать дескриптор, как если бы он был допустимым указателем ЦП, а также анализировать биты внутри дескриптора.

Были добавлены некоторые вспомогательные структуры с элементами инициализации, чтобы упростить управление дескрипторами.

Дескрипторы NULL

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

Остальная часть дескриптора должна быть заполнена как можно больше. Например, в случае представлений ресурсов шейдера (SRV) дескриптор можно использовать для различения типа представления (Texture1D, Texture2D и т. д.). Числовые параметры в дескрипторе представления, например количество MIP-карт, должны быть установлены в значения, допустимые для ресурса.

Во многих случаях существует определенное поведение для доступа к несвязанным ресурсам, таким как SRV, возвращающие значения по умолчанию. Они будут учитываться при доступе к дескриптору NULL, если тип доступа шейдера совместим с типом дескриптора. Например, если шейдер ожидает SRV Texture2D и обращается к SRV NULL, определенному как Texture1D, поведение не определено и может привести к сбросу устройства.

Таким образом, чтобы создать дескриптор NULL, передайте null параметр pResource при создании представления с помощью таких методов, как CreateShaderResourceView. Для параметра описания представления pDesc задайте конфигурацию, которая будет работать, если ресурс не имеет значения NULL (в противном случае может произойти сбой на некотором оборудовании).

Однако корневые дескрипторы не должны иметь значение NULL.

На оборудовании уровня 1 (см. раздел » Аппаратные уровни«, все дескрипторы, привязанные (с помощью таблиц дескриптора) должны быть инициализированы либо как реальные дескрипторы, либо пустые дескрипторы, даже если доступ к оборудованию недоступен, в противном случае поведение не определено.

На оборудовании уровня 2 это относится к привязанным дескрипторам CBV и UAV, но не к дескрипторам SRV.

На оборудовании уровня 3 нет ограничений на это, при условии, что к неинициализированным дескрипторам никогда не обращаются.

Дескрипторы по умолчанию

Чтобы создать дескриптор по умолчанию для конкретного представления, передайте допустимый параметр pResource в метод создания представления (например , CreateShaderResourceView), но передайте значение NULL для параметра pDesc . Например, если ресурс содержал 14 mips, представление будет содержать 14 mips. Вариант по умолчанию охватывает наиболее очевидное сопоставление ресурса с представлением. Для этого требуется, чтобы ресурс был выделен с полным именем формата (например , DXGI_FORMAT_R8G8B8A8_UNORM_SRGB , а не DXGI_FORMAT_R8G8B8A8_TYPELESS).

Дескрипторы по умолчанию нельзя использовать с представлением структуры ускорения лучей, так как предоставленный параметр pResource должен иметь значение NULL, а расположение должно передаваться через [D3D12_RAYTRACING_ACCELERATION_STRUCTURE_SRV]/windows/win32/api/d3d12/ns-d3d12-d3d12_raytracing_acceleration_structure_srv).

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

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

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

Введение и определения

Если говорить в общем, то дескриптор — это атрибут объекта со связанным поведением (англ. binding behavior), т.е. такой, чьё поведение при доступе переопределяется методами протокола дескриптора. Эти методы: __get__ , __set__ и __delete__ . Если хотя бы один из этих методов определён для объекта, то он становится дескриптором.

Стандартное поведение при доступе к атрибутам — это получение, установка и удаление атрибута из словаря объекта. Например, a.x имеет такую цепочку поиска атрибута: a.__dict__[‘x’] , затем в type(a).__dict__[‘x’] , и далее по базовым классам type(a) не включая метаклассы. Если же искомое значение — это объект, в котором есть хотя бы один из методов, определяющих дескриптор, то питон может изменить стандартную цепочку поиска и вызвать один из методов дескриптора. Как и когда это произойдёт зависит от того, какие методы дескриптора определены для объекта. Дескрипторы вызываются только для объектов или классов нового стиля (класс является таким, если наследует от object или type ).

Дескрипторы — это мощный протокол с широкой областью применения. Они являются тем механизмом, который стоит за свойствами, методами, статическими методами, методами класса и вызовом super() . Внутри самого питона с их помощью реализуются классы нового стиля, которые были представлены в версии 2.2. Дескрипторы упрощают понимание нижележащего кода на C, а также представляют гибкий набор новых инструментов для любых программ на питоне.

Протокол дескрипторов

Собственно это всё. Определите любой из этих методов и объект будет считаться дескриптором, и сможет переопределять стандартное поведение, если его будут искать как атрибут.

Если объект определяет сразу и __get__ , и __set__ , то он считается дескриптором данных (англ. data descriptor). Дескрипторы, которые определили только __get__ называются дескрипторами не данных (англ. non-data descriptors). Их называются так, потому что они используют для методов, но другие способы их применения также возможны.

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

Чтобы создать дескриптор данных только для чтения, определите и __get__ , и __set__ , и сделайте так, чтобы __set__ выбрасывал исключение AttributeError . Определения метода __set__ и выбрасывания исключения достаточно, чтобы этот дескриптор считался дескриптором данных.

Вызов дескрипторов

Дескриптор можно вызвать напрямую через его метод. Например, d.__get__(obj) .

Однако, наиболее частый вариант вызова дескриптора — это автоматический вызов во время доступа к атрибуту. Например, obj.d ищет d в словаре obj . Если d определяет метод __get__ , то будет вызван d.__get__(obj) . Вызов будет сделан согласно правилам, описанным ниже.

Детали вызова различаются от того, чем является obj — объектом или классом. В любом случае, дескрипторы работают только для объектов и классов нового стиля. Класс является классом нового стиля, если он является потомком object .

Для объектов алгоритм реализуется с помощью object.__getattribute__ , который преобразует запись b.x в type(b).__dict__[‘x’].__get__(b, type(b)) . Реализация работает через цепочку предшественников, в которой дескрипторы данных имеют приоритет перед переменными объекта, переменные объекта имеют приоритет перед дескрипторами не данных, и самый низкий приоритет у метода __getattr__ , если он определён. Полную реализацию на языке C можно найти в PyObject_GenericGetAttr() в файле Objects/object.c .

Для классов алгоритм реализуется с помощью type.__getattribute__ , который преобразует запись B.x в B.__dict__[‘x’].__get__(None, B) . На чистом питоне это выглядит так:

  • дескрипторы вызываются с помощью метода __getattribute__
  • переопределение __getattribute__ прекратит автоматический вызов дескрипторов
  • __getattribute__ доступен только внутри классов и объектов нового стиля
  • object.__getattribute__ и type.__getattribute__ делают разные вызовы к __get__
  • дескрипторы данных всегда имеют преимущество перед переменными объекта
  • дескрипторы не данных могут потерять преимущество из-за переменных объекта

Примечание: в питоне 2.2, super(B, obj).m() вызывал __get__ только если m был дескриптором данных. В питоне 2.3, дескрипторы не данных тоже вызываются, за исключением тех случаев, когда используются классы старого стиля. Детали реализации можно найти в super_getattro() в файле Objects/typeobject.c , а эквивалент на чистом питоне можно найти в пособии от Guido.

Детали выше описывают, что алгоритм вызова дескрипторов реализуется с помощью метода __getattribute__() для object , type и super . Классы наследуют этот алгоритм, когда они наследуют от object или если у них есть метакласс, реализующий подобную функциональность. Таким образом, классы могут отключить вызов дескрипторов, если переопределят __getattribute__() .

Пример дескриптора

Следующий код создаёт класс, чьи объекты являются дескрипторам данных и всё, что они делают — это печатают сообщение на каждый вызов get или set . Переопределение __getattribute__ — это альтернативный подход, с помощью которого мы могли бы сделать это для каждого атрибута. Но если мы хотим наблюдать только за отдельными атрибутами, то это проще сделать с помощью дескриптора.

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

Свойства

Вызова property() достаточно, чтобы создать дескриптор данных, который вызывает нужные функции во время доступа к атрибуту. Вот его сигнатура:

В документации показано типичное использование property() для создания управляемого атрибута x :

Вот эквивалент property на чистом питоне, чтобы было понятно как реализовано property() с помощью протокола дескрипторов:

Встроенная реализация property() может помочь, когда существовал интерфейс доступа к атрибуту и произошли какие-то изменения, в результате которых понадобилось вмешательство метода.

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

Функции и методы

В питоне все объектно-ориентированные возможности реализованы с помощью функционального подхода. Это сделано совсем незаметно с помощью дескрипторов не данных.

Словари классов хранят методы в виде функций. При определении класса, методы записываются с помощью def и lambda — стандартных инструментов для создания функций. Единственное отличие этих функций от обычных в том, что первый аргумент зарезервирован под экземпляр объекта. Этот аргумент обычно называется self , но может называться this или любым другим словом, которым можно называть переменные.

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

С помощью интерпретатора мы можем увидеть как на самом деле работает дескриптор функции:

Вывод интерпретатора подсказывает нам, что связанные и несвязанные методы — это два разных типа. Даже если они могли бы быть реализованы таким образом, на самом деле, реализация PyMethod_Type в файле Objects/classobject.c содержит единственный объект с двумя различными отображениями, которые зависят только от того, есть ли в поле im_self значение или там содержится NULL (C эквивалент значения None ).

Таким образом, эффект вызова метода зависит от поля im_self . Если оно установлено (т.е. метод связан), то оригинальная функция (хранится в поле im_func ) вызывается, как мы и ожидаем, с первым аргументом, установленным в значение экземпляра объекта. Если же она не связана, то все аргументы передаются без изменения оригинальной функции. Настоящая C реализация instancemethod_call() чуть более сложная, потому что включает в себя некоторые проверки типов и тому подобное.

Статические методы и методы класса

Дескрипторы не данных предоставляют простой механизм для различных вариантов привязки функций к методам.

Повторим ещё раз. Функции имеют метод __get__ , с помощью которых они становятся методами, во время поиска атрибутов и автоматического вызова дескрипторов. Дескрипторы не данных преобразуют вызов obj.f(*args) в вызов f(obj, *args) , а вызов klass.f(*args) становится f(*args) .

В этой таблице показано связывание и два наиболее популярных варианта:

Преобразование Вызвана через объект Вызвана через класс
Дескриптор функция f(obj, *args) f(*args)
staticmethod f(*args) f(*args)
classmethod f(type(obj), *args) f(klass, *args)

Статические методы возвращают функцию без изменений. Вызовы c.f или C.f эквиваленты вызовам object.__getattribute__(c, «f») или object.__getattribute__(C, «f») . Как результат, функция одинаково доступна как из объекта, так и из класса.

Хорошими кандидатами для статических методов являются методы, которым не нужна ссылка на переменную self .

Например, пакет для статистики может включать класс для экспериментальных данных. Класс предоставляет обычные методы для расчёта среднего, ожидания, медианы и другой статистики, которая зависит от данных. Однако, там могут быть и другие функции, которые концептуально связаны, но не зависят от данных. Например, erf(x) это простая функция для преобразования, которая нужна в статистике, но не зависит от конкретного набора данных в этом классе. Она может быть вызвана и из объекта, и из класса: s.erf(1.5) —> 0.9332 или Sample.erf(1.5) —> 0.9332 .

Так как staticmethod() возвращает функцию без изменений, то этот пример не удивляет:

Если использовать протокол дескриптора не данных, то на чистом питоне staticmethod() выглядел бы так:

В отличие от статических методов, методы класса подставляют в начало вызова функции ссылку на класс. Формат вызова всегда один и тот же, и не зависит от того, вызываем мы метод через объект или через класс.

Это поведение удобно, когда нашей функции всегда нужна ссылка на класс и ей не нужны данные. Один из способов использования classmethod() — это создание альтернативных конструкторов класса. В питоне 2.3, метод класса dict.fromkeys() создаёт новый словарь из списка ключей. Эквивалент на чистом питоне будет таким:

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

Если использовать протокол дескриптора не данных, то на чистом питоне classmethod() выглядел бы так:

ООП. Дескрипторы

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

Свойства

Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (property). Рассмотрим следующий пример: пусть у нас есть класс «Профиль пользователя», который включает следующие поля: имя, фамилия и дата рождения.

Из примера видно, что, во-первых, возраст пользователя вычисляется при каждом обращении, во-вторых, мы только получаем значение и никогда его не изменяем. Было бы логично, чтобы клиентский код работал с возрастом как с обычным атрибутом (свойством) доступным только для чтения и python предоставляет нам для этого механизм свойств (propertes):

Таким образом, свойства дают нам возможность создавать, аналогично другим языкам программирования (например, Java), сеттеры и геттеры, а также вычисляемые свойства (computed properties):

Чтобы понять как работают свойства необходимо разобраться с дескрипторами.

Дескрипторы

В документации дано следующее определение дескрипторов:

Дескриптор это любой объект, у которого определены методы __get__() , __set__() или __delete__() . Если дескриптором является атрибут класса, то для него определено специальное поведение при разшенении имени атрибута.

Дескрипторы, которые реализуют только __get__ называются дескрипторами не данных (non-data descriptors), а дескрипторы, которые реализуют __set__ и/или __delete__ называются дескрипторами данных (data descriptors). Рассмотрим следующий пример:

Из примера видно, что при обращении к d1 автоматически был вызван метод __get__ определенный на дескрипторе:

Поведением по умолчанию при доступе к атрибуту является обращение к словарю экземпляра, например, при обращении к a.x поиск начинается с a.__dict__[‘x’] , затем type(a).__dict__[‘x’] и так далее в порядке разрешения методов (mro). Когда же атрибут (класса/метакласса) является дескриптором, то Python изменяет путь поиска, сначала вызывая методы определенные у дескриптора.

Поэтому теперь должно быть понятно, почему при обращении к d2 мы получили просто экземпляр класса. Порядка разрешения имен атрибутов и методов мы коснемся в следующих лекциях.

В Python дескрипторы используются достаточно часто, в том числе и в самом языке, например, функции это дескрипторы:

Это позволяет автоматически передавать экземпляр класса в качестве первого аргумента ( self ), давайте посмотрим на вызов func_descr_get :

Если obj не был передан, то мы имеем дело с обычной функцией, в противном случае это метод и мы «биндим» объект в качестве первого аргумента. На python реализацию функций можно было бы записать так:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *