Руководство к дескрипторам
В этой статье я расскажу о том, что такое дескрипторы, о протоколе дескрипторов, покажу как вызываются дескрипторы. Опишу создание собственных и исследую несколько встроенных дескрипторов, включая функции, свойства, статические методы и методы класса. С помощью простого приложения покажу, как работает каждый из них, приведу эквиваленты внутренней реализации работы дескрипторов кодом на чистом питоне.
Изучение того, как работают дескрипторы, откроет доступ к большему числу рабочих инструментов, поможет лучше понять как работает питон, и ощутить элегантность его дизайна.
Введение и определения
Если говорить в общем, то дескриптор — это атрибут объекта со связанным поведением (англ. 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() выглядел бы так:
Дескрипторы в Python
Сегодня мы поговорим про то, что такое дескрипторы в Python, когда следует их использовать и зачем это вообще нужно.
Дескрипторы Python или, в более общем смысле, просто дескрипторы предоставляют нам мощную технику для написания пригодного к повторному использованию кода, который можно использовать между классами. Они могут показаться похожими на концепцию наследования, но технически это не так. Это универсальный способ перехвата доступа к атрибутам. Дескрипторы — это механизм, лежащий в основе статических методов свойств, методов класса, суперметодов и т. д.
Дескрипторы были добавлены в Python версии 2.2, и с тех пор они считаются волшебными вещами, которые придали традиционным классам новый стиль. Это классы, которые позволяют вам делать управляемыми свойства в другом классе. В частности, они реализуют интерфейс для методов __get__() , __set__() и __delete__() , что делает их интересными по многим причинам.
Что такое дескриптор
Проще говоря, класс, который реализует метод __get__() , __set()__ или __delete()__ для объекта, известен как «дескриптор». Цитируя прямо из официальной документации Python, дескриптор — это атрибут объекта со связанным поведением, то есть такой атрибут, при доступе к которому его поведение переопределяется методом протокола дескриптора. Это методы __get__() , __set__() и __delete__() .
Связанное поведение (англ. binding behavior) применительно к дескрипторам означает привязку способа, которым может устанавливаться, запрашиваться (получаться) или удаляться значение, к данной переменной, объекту или набору данных. Это взаимодействие привязано к части данных, оно применяется только к тем данным, для которых вы его установили.
Дескрипторы можно дополнительно разделить на дескрипторы данных и дескрипторы не-данных. Если дескриптор, который вы пишете, имеет только метод __get__() , то это дескриптор не-данных. А реализация, включающая методы __set__() и __delete__() , называется дескриптором данных. Дескрипторы, не относящиеся к данным, доступны только для чтения, тогда как дескрипторы данных доступны как для чтения, так и для записи.
Важно отметить, что дескрипторы назначаются классу, а не экземпляру класса. При изменении класса перезаписывается или удаляется сам дескриптор, а не активируется его код.
Методы дескриптора
Наконец, класс дескриптора не ограничивается наличием только трех упомянутых методов. То есть он также может содержать любой другой атрибут, кроме методов __get__() , __set__() и __delete__() .
Давайте более подробно разберем методы get() , set() и delete() :
- self — это экземпляр создаваемого вами дескриптора
- object — это экземпляр объекта, к которому прикреплен ваш дескриптор
- type — это тип объекта, к которому присоединен дескриптор
- value — это значение, которое присваивается атрибуту дескриптора. К примеру, get(self, object, type) , set(self, object, value) или delete(self, object)
- __get__() обращается к атрибуту, когда вы хотите извлечь некоторую информацию. Он возвращает значение атрибута или вызывает исключение AttributeError , если запрошенный атрибут отсутствует
- __set__() вызывается в операции присвоения атрибута, которая устанавливает значение атрибута. Данный метод ничего не возвращает, но может вызвать исключение AttributeError
- __delete__() управляет операцией удаления, т. е. служит для удаления атрибута из объекта. Также ничего не возвращает
Теперь давайте разберемся с назначением дескрипторов и разберем это на нескольких примерах!
Назначение дескрипторов
Давайте определим класс Car , имеющий три атрибута, а именно марку, модель и объём топливного бака ( make , model и fuel_cap ). Мы будем использовать метод __init__() для инициализации атрибутов класса. Затем мы воспользуемся волшебной функцией __str__() , которая просто вернет вывод трех атрибутов, которые вы передадите классу при создании объекта.
Обратите внимание, что метод __str__() возвращает строковое представление объекта. Он вызывается, когда для объекта класса вызывается функция print() или str() .
Все выглядит великолепно!
Теперь давайте изменим емкость топливного бака автомобиля на -40. Выглядеть это будет следующим образом:
Подождите, что-то не так, не правда ли? Емкость топливного бака никак не может быть отрицательной. Однако Python принимает этот ввод без ошибок. Это связано с тем, что Python — динамический язык программирования, который не поддерживает явную проверку типов.
Чтобы избежать этой проблемы, давайте добавим условие if в метод __init__() и проверим, является ли введенный объем топливного бака валидным. Если введенный объем невалидный, создадим исключение ValueError . К примеру, это можно сделать так:
Из приведенного выше вывода видно, что на данный момент все работает нормально, поскольку программа выдает ошибку ValueError , если объем топлива ниже нуля.
Однако может возникнуть другая проблема. К примеру, если введенный объем топлива является числом с плавающей запятой или строкой. Целочисленным значением может быть не только запас топлива, но и марка и модель автомобиля. Во всех этих случаях программа не сможет вызвать исключение.
Чтобы справиться с таким случаем, вы можете подумать о добавлении еще одного условия if или, возможно, использовать метод isinstance() для проверки типов.
На этот раз давайте воспользуемся встроенным методом isinstance() для обработки ошибки. К примеру, проверку можно осуществить следующим образом:
Здорово! Таким образом, мы смогли справиться и с этой ошибкой.
Использование дескрипторов
Однако, что делать, если позже вы захотите изменить атрибут топливной емкости на отрицательное значение -40? В данном случае это не сработает, так как проверка типов будет производиться в методе __init__() только один раз. Как вы знаете, метод __init__() является конструктором и вызывается только один раз при создании объекта класса. Следовательно, пользовательская проверка типов позже завершится ошибкой.
Давайте разберемся на примере:
И вот! Вы смогли вырваться из проверки типов.
Теперь подумайте вот о чем. Что, если у вас есть много других атрибутов автомобиля? К примеру, пробег, цена, аксессуары и т. д., которые также требуют проверки типов, и вам также нужна функциональность, в которой некоторые из этих атрибутов имеют доступ только для чтения. Разве это не будет сильно раздражать?
Что ж, для решения всех вышеперечисленных проблем в Python есть дескрипторы!
Как вы узнали выше, любой класс, реализующий магические методы __get__() , __set__() или __delete__() для объекта протокола дескриптора, называется дескриптором. Они также дают вам дополнительный контроль над тем, как атрибут должен работать, например, должен ли он иметь доступ только для чтения или для чтения и записи.
Теперь давайте расширим приведенный выше пример, добавив методы дескриптора. Выглядеть это будет примерно следующим образом:
Не волнуйтесь, если дескриптор класса покажется сперва неясным. Давайте разобьем его на маленькие части и разберемся, что по сути делает каждый метод.
Работа каждого метода дескриптора
Метод __init__() класса дескриптора имеет нулевую локальную переменную __fuel_cap . Двойное подчеркивание в начале означает, что переменная является приватной. Оно нужно для того, чтобы отличить атрибут топливной емкости класса Descriptor от класса Car .
Как вы уже знаете, метод __get__() используется для получения атрибутов и возвращает переменную со значением емкости топлива. Он принимает три аргумента: объект дескриптора, экземпляр класса, содержащего экземпляр объекта дескриптора, т. е. car2 , и, наконец, владельца — класс, к которому принадлежит экземпляр, т. е. класс Car . В этом методе вы просто возвращаете атрибут value , т.е. fuel_cap , значение которого устанавливается в методе __set__() .
Метод __set__() вызывается, когда атрибуту присваивается значение, и, в отличие от метода __get__() , он ничего не возвращает. Он имеет два аргумента помимо самого объекта дескриптора, т. е. экземпляр, совпадающий с методом __get__() , и аргумент value — значение, которое вы присваиваете атрибуту. В этом методе вы проверяете, является ли значение, которое вы хотите присвоить атрибуту fuel_cap , целым числом или нет. Если нет, вы вызываете исключение TypeError . Затем в том же методе вы также проверяете, является ли значение отрицательным. И, если это так, вы вызываете исключение ValueError . После проверки на наличие ошибок вы обновляете атрибут fuel_cap равным значению.
Наконец, метод __delete__() . Он вызывается при удалении атрибута из объекта и аналогичен методу __set__() . Кроме того, он также ничего не возвращает.
Класс Car остается прежним. Единственное изменение, которое вы делаете, это добавление экземпляра fuel_cap класса Descriptor() . Обратите внимание, что, как упоминалось ранее, экземпляр класса дескриптора должен быть добавлен в класс как атрибут класса, а не как атрибут экземпляра.
Как только вы устанавливаете локальную переменную fuel_cap в методе __init__() на экземпляр fuel_cap , она вызывает метод __set__() класса Descriptor .
Тестирование дескриптора
Теперь давайте изменим запас топлива на отрицательное значение и посмотрим, вызовет ли программа исключение ValueError .
Если вы помните, раньше проверка типов при изменении значения атрибута на отрицательное число провалилась, поскольку осуществлялась только один раз, в методе __init__() . Давайте обновим значение fuel_cap до строкового значения и выясним, не приведет ли это к ошибке.
Идеально! Как видите, когда вы позже обновляете атрибут топливной емкости, проверка работает.
Что ж, в дескрипторах есть небольшая проблема, заключающаяся в том, что при создании нового экземпляра или второго экземпляра класса предыдущее значение экземпляра переопределяется. Причина в том, что дескрипторы связаны с классом, а не с экземпляром.
Давайте разберемся в этом на примере ниже.
Когда вы распечатаете экземпляр car2 , вы заметите, что значения были переопределены для car3 .
Заключение
В этой статье мы познакомились с дескрипторами в Python, обсудили принципы их работы и особенности применения. Это руководство было предназначено для тех, кто знаком с Python и стремится освоить продвинутый уровень.
Name already in use
Python-Lessons / Theory / Part_6 / Descriptors.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Дескрипторы и механизм DRY
DRY на помощь для осуществления используя дескрипторы
Когда мы определяем скрытый атрибут для обьекта, чтобы иметь к нему доступ мы определяем геттеры и сеттеры, методы через которые имеем доступ к скрытому свойству.
Мы можем обращаться к этим методам и вызывать их из вне.
Класс property позволяет нам создать обьект который будет храниться как атрибут класса, этот атрибут будет своего рода псевдонимом для нужного нам атрибута обьекта.
Далее мы связываем наши методы геттеры и сеттеры и метод для удаления с этим псевдонимом, и далее производя вставку или извлечение или удаление этого псевдонима, то будут вызываться методы связанные с ним.
Вот как это работает:
Вот что мы имеем, у нас есть 3 метода: сеттер, геттер и для удаления атрибута обьекта self.x
- __getCoordX — вставка
- __setCoordX — кладет
- __delCoordX — удаляет
Эти методы могут быть как защищенные так и обычные, все они работают с одним и тем же атрибутом обьекта, далее мы связываем их через:
Первый параметр это геттер, которой сеттер и третий для удаления, после этого у нас в классе будет создан атрибут coordX к которому и будут привязаны все 3 метода.
И теперь когда мы выполняем операции с этим новым атрибутом, будут работать связанные с ним методы.
Данный процесс связывания должен происходить в самом конце класса, ибо язык интерпретируемый и этих методов не будет существовать на момент связывания если мы укажем их в конце.
Обьекты-свойства через декораторы
Помимо обычного использования property их можно использовать через декораторы, след способом:
Тут все очень интересно, раньше каждому методу мы давали уникальное название, теперь все методы будут иметь одно название, и это название того свойства в котором мы хранили обьект property то есть все методы будут иметь одно и то же название, в данном случае это coordX
Теперь все методы имеют одно название, и далее мы обрамляем их декораторами, тут есть ряд правил:
Метод, который выполняет роль геттера, является базовым, он получает декоратор @property
Все остальные методы получают декоратор в виде названия того свойства в котором хранится обьект property то есть декоратор вида @coordX
Остальные методы получаю декораторы с дополнительным именем.
Геттер — @property Сеттер — @coordX.setter Удаление — @coordX.deleter
Таким образом мы получаем все 3 метода.
Вот пример такой программы:
property и принципы DRY
Но все эти методы выполняют обработку свойства coordX которое отвечает за атрибут объекта self.x В таком случае если мне понадобится сделать методы для self.y должен ли я определить еще 3 метода для него ?
Да это можно сделать, но это будет избыточным решением, и нарушением принцыпа DRY(не повторять свой код).
Для решения этой задачи существует способ Дескриптор классов
Дескрипторы часто используются в ORM или фреймворках, но самому писать их приходится довольно редко.
Если говорить о сути Дескрипторов, то это класс обьект которого становится атрибутом в классе что его использует, суть класса дескриптора в том что при помощи спец магических методов, и реализует создание, вставку, извлечение или удаление некого значения, которое будет содержаться как атрибут в базовом классе.
Магические методы Дескрипторов:
__set__(self, instance, value) Вносит значение в атрибут главного класса.
__get__(self, instance, owner) Возвращает значение атрибута из главного класса.
__delete__(self, obj) Удаляет его.
__set_name__(self, owner, name) Для присваивания атрибуту главного класса того же имени, что и у обьекта в котором хранится экземпляр Дескриптора. name — обладает именем атрибута в котором
Главная суть этих методов, в том что аргумент instance определяется питоном автоматически, и содержит в себе экземпляр того класса который использует дескриптор.
Общая суть дескрипторов заключается в том, чтобы сократить размер кода, и позволить использовать один набор методов Дескриптора для множества различных атрибутов.
Как видим в главном классе мы добавляем атрибут coordX = CoordValue(‘coordX’) в котором будет храниться обьект дескриптора, и в него передает название coordX именно это название и будет использовано для создания атрибута обьекта для главного класса.
Атрибут instance хранит обьект главного класса, по этому через него мы и создаем новый атрибут обьекта, в методе __init__ хранится название по которому будет создаваться атрибут, название класса дескриптора само по себе не имеет значения.
Начиная с 3.6 версии питона, появился еще один метод __set_name__() его суть проста, он позволяет автоматизировать создание одноименного атрибута.
Новый класс Дескриптора:
Метод инициализатор более не нужен, имя вносится при помощи __set_name__
Новый главный класс:
Как видим можно убрать название, оно будет передано автоматически.
Вызовем работу класса:
Вот что у нас происходит, атрибут в котором хранится дескриптор, называется coordX и атрибут обьекта в котором будет храниться значение созданное дескриптором будет иметь название coordX
При обращении к атрибуту:
Питон автоматических отдает приоритет этого вызова дескриптору, а не атрибуту обьекта, это реализовано автоматически.
2 Типа Дескрипторов
В питоне существуют Дескрипторы 2 типов:
Дескриптор данных — реализует все методы работы с данными
Дескриптор не данных — реализован только один метод __get__() и метод __set_name__()
Суть Дескриптора не данных, в том, чтобы возвращать всегда статичное значение.
ООП. Дескрипторы
В этой лекции мы рассмотрим такой важный механизм как дескрипторы, а также разберемся с тем как же устроены методы класса.
Свойства
Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (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 реализацию функций можно было бы записать так: