Что такое дескриптор в python
Еще немного о дескрипторах в Python
Не так давно на Хабре уже был перевод статьи Раймонда Хеттингера Руководство к дескрипторам. В этой статье я постараюсь рассмотреть вопросы, которые возникли у меня после прочтения. Будет немного примеров кода, собственно вопросов и ответов к ним. Для понимания того, о чем речь, вам нужно знать, что такое дескрипторы и зачем они.
Когда вызываются дескрипторы?
Рассмотрим следующий код:
>>> class M(type):
. def __new__(cls, name, bases, dct):
. dct[‘test’] = lambda self, x: x*2
. return type.__new__(cls, name, bases, dct)
.
>>> class A(object):
. def __init__(self):
. self.__dict__[‘test2’] = lambda self, x: x*4
. __metaclass__ = M
.
>>> A().test(2)
4
>>> A().test2(2)
Traceback (most recent call last):
File » stdin > «, line 1, in module >
TypeError: lambda > () takes exactly 2 arguments (1 given)
Что не так? Вроде добавляем функцию одинаково, используя словарь объекта. Почему не вызывается дескриптор для функции «test2»? Дело в том, что функция «test» определяется для словаря класса, а функция «test2» — для словаря объекта:
>>> ‘test’ in A.__dict__
True
>>> ‘test2’ in A.__dict__
False
>>> ‘test’ in A().__dict__
False
>>> ‘test2’ in A().__dict__
True
Отсюда — первый ответ: функция «__get__» вызывается только для дескрипторов, которые являются свойствами класса, а не свойствами объектов этого класса.
Что является дескриптором?
Предыдущий ответ сразу вызывает вопрос — что значит «только для дескрипторов»? Я ведь не создавал никаких дескрипторов, я создал только функцию!
Ответ ожидаемий — функции в Питоне являются дескрипторами(если быть точнее — дескрипторами не данных):
Bound/Unbound методы
И напоследок самое вкусное. Задача:
Есть объект некоего класса, к которому необходимо динамически добавить метод, которому, конечно, должен передаваться «self» параметр. Не представляется возможным добавлять этот метод в словарь класса (мы не хотим повлиять на другие объекты).
Решить «в лоб» не выходит:
>>> class A(object):
. def __init__(self):
. self.x = 3
.
>>> def add_x(self, add):
. self.x += add
. print ‘Modified value: %s’ % (self.x,)
.
>>> a = A()
>>> a.add_x = add_x
>>> a.x
3
>>> a.add_x(3)
Traceback (most recent call last):
File » stdin > «, line 1, in module >
TypeError: add_x() takes exactly 2 arguments (1 given)
Получаем ожидаемую ошибку, которая подтверждает первый ответ — при обращении к свойству-дескриптору объекта не используется «__get__». Что же делать?
Поиграем немного с методом «__get__» функции, которую мы только что создали:
О, кажется это то, что нам надо! В предпоследней строчке мы получили «bound» метод — именно так смотрелся бы вызов «A().add_x», если в классе «A» был бы определен метод «add_x». А в последней строчке видим «ожидаемый» результат вызова «A.add_x». Теперь мы знаем, как добавить метод к объекту:
Бинго!
И так, именно метод «__get__» для функций-дескрипторов создает «bound/unbound» методы классов и объектов. И нет здесь никакой магии 🙂
Литература
Что еще почитать? На самом деле — немного. Есть несколько глав, в которых рассказывается о дескрипторах, в Python Data Model (implementing-descriptors), ну и можно снова вспомнить действительно хорошую статью Хеттингера (оригинал).
HOWTO по дескрипторам¶
Аннотация¶
Определяет дескрипторы, резюмирует протокол и показывает, как дескрипторы вызываются. Исследует пользовательский дескриптор и несколько встроенных дескрипторов Python, включая функции, свойства, статические методы и методы классов. Показывает, как работает каждый из них, предоставляя эквивалент на чистом Python и образец приложения.
Изучение дескрипторов не только предоставляет доступ к большему набору инструментов, но и даёт более глубокое понимание того, как работает Python, и понимание элегантности его дизайна.
Определение и введение¶
Дескрипторный протокол¶
Вот и всё. Определите любой из этих методов, и объект будет считаться дескриптором и может переопределить поведение по умолчанию при поиске в качестве атрибута.
Дескрипторы без данных и дескрипторы данных различаются по способу вычисления переопределений по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и дескриптор данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и дескриптор без данных, запись словаря будет приоритетнее.
Вызов дескрипторов¶
Детали вызова зависят от того, является ли obj объектом или классом.
Важные моменты, о которых следует помнить:
Подробности реализации находятся в: c:func: super_getattro() в Objects/typeobject.c, а эквивалент на чистом Python можно найти в Учебнике Гвидо.
Пример дескриптора¶
Следующий код создаёт класс, объекты которого являются дескрипторами данных, которые печатают сообщение при каждом получения или установки. Переопределение __getattribute__() — это альтернативный подход, который может сделать это для каждого атрибута. Однако этот дескриптор полезен для мониторинга только нескольких выбранных атрибутов:
Протокол прост и предлагает захватывающие возможности. Некоторые варианты использования настолько распространены, что были объединены в отдельные вызовы функций. Свойства, связанные методы, статические методы и методы классов основаны на протоколе дескриптора.
Свойства¶
Вызов property() — это краткий способ создания дескриптора данных, который запускает вызовы функций при доступе к атрибуту. Его сигнатура:
В документации показано типичное использование для определения управляемого атрибута x :
Чтобы увидеть, как property() реализован в терминах протокола дескриптора, в виде его чистого эквивалента Python:
Встроенная функция property() помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибутам, а затем последующие изменения требуют вмешательства метода.
Функции и методы¶
Объектно-ориентированные функции Python построены на среде, основанной на функциях. Используя дескрипторы без данных, два подхода легко объединяются.
Для поддержки вызовов методов функции включают метод __get__() для привязки методов во время доступа к атрибутам. Это означает, что все функции не являются дескрипторами данных, которые возвращают связанные методы, когда они вызываются из объекта. В чистом Python это работает так:
Запуск интерпретатора показывает, как дескриптор функции работает на практике:
Статические методы и методы классов¶
Дескрипторы без данных предоставляют простой механизм для вариаций обычных шаблонов связывания функций с методами.
В этой таблице показаны два наиболее полезных варианта привязки:
Преобразование | Вызывается из объекта | Вызывается из класса |
---|---|---|
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
Поскольку статические методы возвращают базовую функцию без изменений, вызовы примеров неинтересны:
При использовании протокола дескриптора без данных чистая версия Python staticmethod() будет выглядеть так:
В отличие от статических методов, методы класса добавляют ссылку на класс к списку аргументов перед вызовом функции. Этот формат одинаков для того, является ли вызывающий объект объектом или классом:
Это поведение полезно, когда у функции должна быть только ссылка на класс и не заботится о каких-либо базовых данных. Одно из применений методов классов — создание альтернативных конструкторов классов. В Python 2.3 метод классов dict.fromkeys() создаёт новый словарь из списка ключей. Эквивалент на чистом Python:
Теперь можно построить новый словарь уникальных ключей:
При использовании протокола дескриптора без данных чистая версия Python classmethod() будет выглядеть так:
Атрибуты и протокол дескриптора в Python
Рассмотрим такой код:
Сегодня мы разберём ответ на вопрос: «Что именно происходит, когда мы пишем foo.bar?»
Вы, возможно, уже знаете, что у большинства объектов есть внутренний словарь __dict__, содержащий все их аттрибуты. И что особенно радует, как легко можно изучать такие низкоуровневые детали в Питоне:
Давайте начнём с попытки сформулировать такую (неполную) гипотезу:
Пока звучит похоже на правду:
Теперь предположим, что вы уже в курсе, что в классах можно объявлять динамические аттрибуты:
Хм… ну ладно. Видно что __getattr__ может эмулировать доступ к «ненастоящим» атрибутам, но не будет работать, если уже есть объявленная переменная (такая, как foo.bar, возвращающая ‘hello!’, а не ‘goodbye!’). Похоже, всё немного сложнее, чем казалось вначале.
И действительно: существует магический метод, который вызывается всякий раз, когда мы пытаемся получить атрибут, но, как продемонстрировал пример выше, это не __getattr__. Вызываемый метод называется __getattribute__, и мы попробуем понять, как в точности он работает, наблюдая различные ситуации.
Пока что модифицируем нашу гипотезу так:
foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:
Проверим практикой, реализовав этот метод (под другим именем) и вызывая его напрямую:
Выглядит корректно, верно?
Отлично, осталось лишь проверить, что поддерживается присвоение переменных, после чего можно расходиться по дом… —
my_getattribute возвращает некий объект. Мы можем изменить его, если он мутабелен, но мы не можем заменить его на другой с помощью оператора присвоения. Что же делать? Ведь если foo.baz это эквивалент вызова функции, как мы можем присвоить новое значение атрибуту в принципе?
Когда мы смотрим на выражение типа foo.bar = 1, происходит что-то больше, чем просто вызов функции для получения значения foo.bar. Похоже, что присвоение значения атрибуту фундаментально отличается от получения значения атрибута. И правда: мы может реализовать __setattr__, чтобы убедиться в этом:
Пара вещей на заметку относительно этого кода:
А ведь у нас есть ещё и property (и его друзья). Декоратор, который позволяет методам выступать в роли атрибутов.
Давайте постараемся понять, как это происходит.
Просто ради интереса, а что у нас в f.__dict__?
В __dict__ нет ключа bar, но __getattr__ почему-то не вызывается. WAT?
bar — метод, да ещё и принимающий в качестве параметра self, вот только это метод находится в классе, а не в экземпляре класса. И в этом легко убедиться:
Ключ bar действительно находится в словаре атрибутов класса. Чтобы понять работу __getattribute__, нам нужно ответить на вопрос: чей __getattribute__ вызывается раньше — класса или экземпляра?
Видно, что первым делом проверка идёт в __dict__ класса, т.е. у него приоритет перед экземпляром.
Погодите-ка, а когда мы вызывали метод bar? Я имею в виду, что наш псевдокод для __getattribute__ никогда не вызывает объект. Что же происходит?
Вся суть тут. Реализуйте любой из этих трёх методов, чтобы объект стал дескриптором и мог менять дефолтное поведение, когда с ним работают как с атрибутом.
Если объект объявляет и __get__(), и __set__(), то его называют дескриптором данных («data descriptors»). Дескрипторы реализующие лишь __get__() называются дескрипторами без данных («non-data descriptors»).
Оба вида дескрипторов отличаются тем, как происходит перезапись элементов словаря атрибутов объекта. Если словарь содержит ключ с тем же именем, что и у дескриптора данных, то дескриптор данных имеет приоритет (т.е. вызывается __set__()). Если словарь содержит ключ с тем же именем, что у дескриптора без данных, то приоритет имеет словарь (т.е. перезаписывается элемент словаря).
Чтобы создать дескриптор данных доступный только для чтения, объявите и __get__(), и __set__(), где __set__() кидает AttributeError при вызове. Реализации такого __set__() достаточно для создания дескриптора данных.
Короче говоря, если вы объявили любой из этих методов — __get__, __set__ или __delete__, вы реализовали поддержку протокола дескриптора. А это именно то, чем занимается декоратор property: он объявляет доступный только для чтения дескриптор, который будет вызываться в __getattribute__.
Последнее изменение нашей реализации:
foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:
Попробуем продемонстрировать на практике:
Мы лишь немного поскребли поверхность реализации атрибутов в Python. Хотя наша последняя попытка эмулировать foo.bar в целом корректна, учтите, что всегда могут найтись небольшие детали, реализованные по-другому.
Надеюсь, что помимо знаний о том, как работают атрибуты, мне так же удалось передать красоту языка, который поощряет вас к экспериментам. Погасите часть долга знаний сегодня.
ООП. Дескрипторы
В этой лекции мы рассмотрим такой важный механизм как дескрипторы, а также разберемся с тем как же устроены методы класса.
Свойства
Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (property). Рассмотрим следующий пример: пусть у нас есть класс «Профиль пользователя», который включает следующие поля: имя, фамилия и дата рождения.
Из примера видно, что, во-первых, возраст пользователя вычисляется при каждом обращении, во-вторых, мы только получаем значение и никогда его не изменяем. Было бы логично, чтобы клиентский код работал с возрастом как с обычным атрибутом (свойством) доступным только для чтения и python предоставляет нам для этого механизм свойств (propertes):
Таким образом, свойства дают нам возможность создавать, аналогично другим языкам программирования (например, Java), сеттеры и геттеры, а также вычисляемые свойства (computed properties):
Чтобы понять как работают свойства необходимо разобраться с дескрипторами.
Дескрипторы
В документации дано следующее определение дескрипторов:
Дескрипторы, которые реализуют только __get__ называются дескрипторами не данных (non-data descriptors), а дескрипторы, которые реализуют __set__ и/или __delete__ называются дескрипторами данных (data descriptors). Рассмотрим следующий пример:
Из примера видно, что при обращении к d1 автоматически был вызван метод __get__ определенный на дескрипторе:
Поэтому теперь должно быть понятно, почему при обращении к d2 мы получили просто экземпляр класса. Порядка разрешения имен атрибутов и методов мы коснемся в следующих лекциях.
В Python дескрипторы используются достаточно часто, в том числе и в самом языке, например, функции это дескрипторы:
Это позволяет автоматически передавать экземпляр класса в качестве первого аргумента ( self ), давайте посмотрим на вызов func_descr_get :
Если obj не был передан, то мы имеем дело с обычной функцией, в противном случае это метод и мы «биндим» объект в качестве первого аргумента. На python реализацию функций можно было бы записать так:
А вот примеры реализаций декораторов @staticmethod и @classmethod:
И наконец реализация @property:
Пример простой ORM можно найти в репозитории с лекциями.
Русские Блоги
Обучение дескрипторов в Python
Что такое дескриптор
ДескрипторЭто одна из ключевых особенностей классов Python в новом стиле. Он предоставляет мощные API-интерфейсы для атрибутов объектов. Вы можете подумать оДескрипторАгент, представляющий атрибуты объекта. Когда вам нужен атрибут, вы можете получить к нему доступ через дескриптор в соответствии с ситуацией, с которой вы столкнулись (взято из программирования ядра Python).
Пример анализа
Используйте методы класса для создания дескрипторов
Которые только достигли __set__() Метод рассматривается как дескриптор метода или дескриптор, не связанный с данными. Достигнутые одновременно __set__() с участием __get__() Класс методов называется дескриптором данных.
Сначала определите класс дескриптора данных
Определим класс, который вызывает дескриптор данных
Обнаружено, что при доступе к атрибуту desc Myclass дескриптор __get__() метод. Этим достигается роль дескрипторов (доступ к свойствам объекта можно изменять).
Принцип вызова: для дескрипторов атрибутов класса, если синтаксический анализатор обнаруживает, что атрибут x является дескриптором, он будет проходить внутреннюю type.__getattribute__() (Вызывается безоговорочно при доступе к атрибутам, вызывается первым), он может Class.x Перевести в Class.__dict__[‘x’].__get__(None, Class) Приходите
Дескриптор определяется как атрибут класса выше. Каковы сходства и различия, когда мы определяем его как атрибут объекта?
Называется не так, как мы ожидали __get__() Method, просто скажите, что он является объектом Descriptor.
Так каков же эффект, если определение объекта дескриптора атрибута класса и имена атрибутов экземпляра одинаковы?
Результаты приведены ниже:
Видно, что к дескриптору обращаются во время инициализации. __set__() Метод, доступ к дескриптору __get__() метод. Так зачем снова вызывать метод дескриптора?
Чтобы объяснить эту проблему, давайте сначала поговорим о python Приоритет доступа к собственности в следующем:
Затем мы распечатываем список атрибутов вышеуказанного класса кода и экземпляра:
Результаты приведены ниже:
Согласно теории приоритета доступа к атрибутам, описанной выше, дескриптор данных> атрибут экземпляра. Когда python обнаруживает, что существует объект с тем же именем, что и определенный дескриптор в словаре объекта экземпляра, дескриптор имеет приоритет, а атрибут экземпляра будет перезаписан. Python перепишет поведение по умолчанию и вместо этого вызовет метод дескриптора.
Видно, что доступ к свойствам экземпляра в этом случае не вызывает дескриптор __get__() метод. Вместо этого он вызывает свои собственные свойства. Видно, что теория верна.
Создать дескриптор с использованием типа атрибута
property Встроенная функция имеет четыре параметра: property(fget=None, fset=None, fdel=None, doc=None) 。
fget: метод приобретения собственности
fset: метод установки свойства
fdel: метод удаления атрибута
doc: описание документа
Взглянем на реализацию:
Результаты приведены ниже:
Реализация дескриптора таким образом, хотя и проста. Но если атрибутов много, код будет раздутым.
Используйте модификаторы атрибутов для создания дескрипторов
Результаты приведены ниже:
Видите, код работает так же, как и раньше. Конкретный принцип повторяться не будет. Можно распечатать PropertyDesc Класс и экземпляр pro Подумайте о списке атрибутов.