Что такое дескрипторы в питоне
Еще немного о дескрипторах в 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), ну и можно снова вспомнить действительно хорошую статью Хеттингера (оригинал).
Руководство к дескрипторам
Краткий обзор
В этой статье я расскажу о том, что такое дескрипторы, о протоколе дескрипторов, покажу как вызываются дескрипторы. Опишу создание собственных и исследую несколько встроенных дескрипторов, включая функции, свойства, статические методы и методы класса. С помощью простого приложения покажу, как работает каждый из них, приведу эквиваленты внутренней реализации работы дескрипторов кодом на чистом питоне.
Изучение того, как работают дескрипторы, откроет доступ к большему числу рабочих инструментов, поможет лучше понять как работает питон, и ощутить элегантность его дизайна.
Введение и определения
Протокол дескрипторов
Собственно это всё. Определите любой из этих методов и объект будет считаться дескриптором, и сможет переопределять стандартное поведение, если его будут искать как атрибут.
Дескрипторы данных и не данных отличаются в том, как будет изменено поведение поиска, если в словаре объекта уже есть запись с таким же именем как у дескриптора. Если попадается дескриптор данных, то он вызывается раньше, чем запись из словаря объекта. Если в такой же ситуации окажется дескриптор не данных, то запись из словаря объекта имеет преимущество перед этим дескриптором.
Вызов дескрипторов
Пример дескриптора
Этот простой протокол предоставляет просто увлекательные возможности. Некоторые из них настолько часто используются, что были объединены в отдельные функции. Свойства, связанные и несвязанные методы, статические методы и методы класса — все они основаны на этом протоколе.
Свойства
Вызова property() достаточно, чтобы создать дескриптор данных, который вызывает нужные функции во время доступа к атрибуту. Вот его сигнатура:
В документации показано типичное использование property() для создания управляемого атрибута x :
Вот эквивалент property на чистом питоне, чтобы было понятно как реализовано property() с помощью протокола дескрипторов:
Встроенная реализация property() может помочь, когда существовал интерфейс доступа к атрибуту и произошли какие-то изменения, в результате которых понадобилось вмешательство метода.
Функции и методы
В питоне все объектно-ориентированные возможности реализованы с помощью функционального подхода. Это сделано совсем незаметно с помощью дескрипторов не данных.
С помощью интерпретатора мы можем увидеть как на самом деле работает дескриптор функции:
Вывод интерпретатора подсказывает нам, что связанные и несвязанные методы — это два разных типа. Даже если они могли бы быть реализованы таким образом, на самом деле, реализация PyMethod_Type в файле Objects/classobject.c содержит единственный объект с двумя различными отображениями, которые зависят только от того, есть ли в поле im_self значение или там содержится NULL (C эквивалент значения None ).
Статические методы и методы класса
Дескрипторы не данных предоставляют простой механизм для различных вариантов привязки функций к методам.
Так как staticmethod() возвращает функцию без изменений, то этот пример не удивляет:
Если использовать протокол дескриптора не данных, то на чистом питоне staticmethod() выглядел бы так:
В отличие от статических методов, методы класса подставляют в начало вызова функции ссылку на класс. Формат вызова всегда один и тот же, и не зависит от того, вызываем мы метод через объект или через класс.
Это поведение удобно, когда нашей функции всегда нужна ссылка на класс и ей не нужны данные. Один из способов использования classmethod() — это создание альтернативных конструкторов класса. В питоне 2.3, метод класса dict.fromkeys() создаёт новый словарь из списка ключей. Эквивалент на чистом питоне будет таким:
Теперь новый словарь уникальных ключей можно создать таким образом:
Если использовать протокол дескриптора не данных, то на чистом питоне classmethod() выглядел бы так:
Дескрипторы классов в Python.
Руководство по использованию дескрипторов класса.
Содержание:
Протокол дескрипторов класса.
Вот и все, что нужно сделать. Определите любой из этих методов и объект будет считаться дескриптором и может переопределить поведение по умолчанию при поиске в качестве атрибута.
Дескрипторы данных и дескрипторы без данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если словарь экземпляра содержит запись с тем же именем, что и дескриптор данных, то дескриптор данных имеет приоритет. Если словарь экземпляра содержит запись с тем же именем, что и дескриптор без данных, то уже запись будет имеет приоритет.
Вызов дескрипторов класса.
Детали вызова зависят от того, является ли obj объектом или классом.
Важно помнить следующее:
Пример дескриптора класса.
Протокол дескрипторов класса очень прост и предлагает огромные возможности. Некоторые варианты использования настолько распространены, что были объединены в отдельные вызовы функций.
На протоколе дескриптора основаны: свойства, связанные методы, статические методы и методы классов!
Дескрипторы данных в классе (дескрипторы атрибутов).
В документации показано типичное использование для определения управляемого атрибута x :
Чтобы увидеть, как функция property() реализована с точки зрения протокола дескриптора класса, вот чистый эквивалент на Python:
Встроенная функция property() помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибутам, а затем последующие изменения требуют вмешательства метода.
Классно, неправда ли?
Дескрипторы функций и методы класса.
Объектно-ориентированные функции Python построены на среде, основанной на функциях. Используя дескрипторы, не относящиеся к данным, они легко объединяются.
В чистом Python это работает так:
Запуск интерпретатора показывает, как дескриптор функции работает на практике:
Дескрипторы, не относящиеся к данным.
Дескрипторы, не относящиеся к данным, предоставляют простой механизм для вариаций обычных шаблонов привязки функций к методам.
На этой диаграмме представлена привязка и два ее наиболее полезных варианта:
Преобразования | Вызов из объекта | Вызов из класса |
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
Статический метод как дескриптор без данных.
Так как статические методы возвращают базовую функцию без изменений, то вызовы примеров неинтересны:
При использовании протокола дескриптора не относящегося к данным, чистая версия staticmethod() для Python будет выглядеть так:
Метод класса как дескриптор без данных.
В отличие от статических методов, методы класса добавляют ссылку на класс к списку аргументов перед вызовом функции. Этот формат одинаков для того, является ли вызывающий объект объектом или классом:
Что такое дескрипторы и их использование в Python 3.6+
Что такое дескрипторы? Очень частый вопрос на собеседованиях. Сложность вопроса в том что реально в своих проектах почти ни кто не использует дескрипторы. Вы можете проработать все жизнь программистом python и ни разу не задействовать их ни в одном своем проекте. Но при этом вы будете почти постоянно использовать их через подключаемые сторонние библиотеки. Обычно говориться если вы захотели использовать их, остановитесь и лучше подумайте об архитектуре проекта. Возможно существует множество других более простых решений. И в большинстве случаев так и будет, за не большим исключением. Дескрипторы традиционно используются только если вы создаете ORM или новый фреймворк. Поэтому знать о них нужно, но не столько для их использования, а больше для понимания как работает магия python.
В сети можно найти множество статей описывающих что такое дескрипторы. Но большинство их них имеют достаточно перегруженное, сухое и не особо понятное описание. Поэтому я нашел и перевел, как мне кажется неплохое объяснение через использование примеров. Оригинал статьи
Вы видели похожий код или, может быть даже писали что-то подобное?
Этот небольшой фрагмент был частично взят из учебника по популярной ORM библиотеки SQLAlchemy. Подобный код можно встреть наверно в любой ORM в python. А вы когда-нибудь задумывались, почему атрибуты id и name не передаются через метод __init__ и потом не привязываются к экземпляру класса, как это обычно делается в классе. Если да то в этой статье я расскажу, как и зачем это делается.
Это описание можно встретить почти в каждой статье о дескрипторах. Но в реальности оно не особо понятное если читаешь его первый раз.
Попробуем рассказать о дескрипторах чуть проще. В python существует три варианта доступа к атрибуту. Допустим у нас есть атрибут a объекта obj :
Python позволяет перехватить выше упомянутые попытки доступа к атрибуту и переопределить связанное с этим доступом поведение. Это реализуется через механизм протокола дескрипторов.
Зачем нам нужны дексрипторы?
Давайте рассмотрим пример:
Что не так с этим кодов? Если этот код начать использовать, мы столкнемся с проблемой. Наши данные ни как не проверяются. То есть цена (price) и количество (quantity) может принимать любое значение:
Вместо того чтобы использовать методы getter и setter и создавать новое API, давайте используем стандартный декоратор property для проверки значения атрибута quantity:
Как использовать дескрипторы
При использовании дескрипторов наше новое определение класса станет таким:
Обратите внимание на атрибуты класса определенные до метода __init__ Это очень похоже на пример от SQLAlchemy приведенный в начале статьи. Теперь нам нужно создать класс NonNegative и реализовать протокол дескрипторов:
Позже мы увидим, как в Python 3.6+ мы можем избежать текущей избыточности кода.
Избыточности можно было бы избежать в более ранних версиях Python, но я думаю, что для объяснения потребуется слишком много усилий, и цель этого поста не в этом.
Добро пожаловать в Python 3.6+
Давайте используем новый протокол дескрипторов появившийся в Python 3.6:
С этим протоколом, мы можем удалить __init__ и привязать имя атрибута к дескриптору:
Теперь окончательная версия нашего кода:
Заключение
Python — это язык программирования общего назначения. Мне нравится, что он не только обладает очень мощными функционалом, которые очень гибок (например, использование мета-классов), а также имеет высокоуровневое API для решения 99% потребностей (например, те же дескрипторов). Дескрипторы, безусловно, являются хорошим инструментом для привязки поведения к атрибутам. Хотя метаклассы потенциально могут делать то же самое, дескриптор может решить проблему более изящно.
Python: сложные аспекты
Впервые было опубликовано в журнале «Системный администратор» #5 за 2009 год
Рассматриваем метаклассы, дескрипторы атрибутов и менеджеры контекста.
В этой статье мы рассмотрим некоторые достаточно сложные аспекты языка Python, а именно:
Метаклассы
«Новые» классы
Новая система типов и классов (так называемые «новые» классы) были добавлены в Python 2.2 для унификации классов и типов. Основная причина их появления — это предоставление унифицированной объектной модели с полноценной моделью метаклассов. «Новые» классы так же предоставляют следующие возможности:
Начиная с Python 3.0, «старые» классы были удалены и по умолчанию используются «новые» классы (которые уже нет необходимости называть «новыми»).
В общем случае, как и следует из названия, метаклассы — это классы классов. Таким образом классы являются экземплярами метаклассов. Начиная с Python 2.2 стандартным метаклассом является type, который служит метаклассом для всех встроенных типов. Это можно увидеть на следующем примере:
Здесь классом для создания кортежа является tuple и соответственно классом для создания tuple является type. В этой статье мы рассматриваем только, так называемые «новые» классы, т.е. классы которые наследуются от встроенного класса object. На данный момент «старые» (или «классические») классы должны представлять только исторический интерес, хотя они еще используются в некоторых проектах.
В Python при выполнении выражения описывающего класс, интерпретатор сначала определяет соответствующий классу метакласс M и затем вызывает M(name, bases, dict) для создания класса. Это происходит после того как было обработано тело класса, где определены его методы и атрибуты. Аргументами при вызове метакласса являются:
Затем результат вызова M присваивается переменной с именем класса. Описание вызова метакласса для создания класса можно проиллюстрировать следующим примером:
После того как мы рассмотрели как метакласс создает класс, остается понять, как выбирается метакласс. Для выбора метакласса используются следующие шаги:
Начиная с Python 3.0 метакласс можно указывать только как именованный параметр при определении класса, следующим образом:
Основные ограничения связанные с метаклассами Python:
Примеры метаклассов
После описания работы метаклассов обратимся к примерам собственных реализаций. Как уже было рассмотрено ранее, класс создается при вызове метакласса следующим образом: M(name, bases, dict). Более детально, при создании классов (можно провести аналогию с созданием объектов класса) вызываются методы метакласса __new__() и затем __init__(), как в следующей последовательности строк:
Напишем наш первый метакласс, чтобы рассмотреть последовательность вызова методов при создании класса и объекта:
Здесь мы просто выводим информацию о вызове методов __new__(), __init__() и __call__(). Вот как это работает:
Обратите внимание на атрибут __metaclass__ в теле класса, как уже было описано выше, это один из способов присвоения метакласса классу. Таким образом, мы видим последовательность вызова методов метакласса:
Нужно так же отметить, что атрибуты и методы, определенные в метаклассе являются статическими, т.е. доступны только на уровне класса, но не на уровне объектов класса:
Рассмотрим примеры более полезных метаклассов. Метакласс AutoSuper добавляет приватный атрибут __super для доступа к атрибутам и методам базовых классов:
Теперь он может быть использован следующим образом:
Таким образом при работе с классом и его подклассами можно везде заменить вызов встроенной функции super на обращение в приватному атрибуту __super. Это позволяет контролировать доступ к базовым классам на уровне класса, или даже объекта. Плюс к этому, уменьшается вероятность ошибок связанных с опечатками и, в случае изменения имени класса, нет необходимости изменять имя в нескольких местах.
Следующий пример представляет из себя метакласс, устанавливающий атрибуты для объектов создаваемых классом без необходимости определения конструктора класса:
Этот метакласс может быть использован следующим образом:
Такой метакласс может быть полезен для создания классов объекты которых служат в основном как хранилище атрибутов. Например, классов описывающих передаваемые по сети пакеты данных, или строки результата запроса к базе данных к полям которых удобнее обращаться как к атрибутам.
Таким образом, метаклассы позволяют создавать классы с достаточно необычным поведением, но в тоже время вряд ли стоит их использовать в каждой программе.
Дескрипторы атрибутов
Дескрипторы атрибутов (далее просто дескрипторы) описывают протокол доступа к атрибутам объекта, или класса. В общем случае дескрипторы — это объекты, в которых определен один из методов: __get__(), __set__(), или __delete__(). Среди уже определенных в Python дескрипторов можно назвать следующие: property, classmethod и staticmethod. Рассмотрим интерфейс дескрипторов на примере:
При доступе к атрибуту методы этого дескриптора вызываются следующим образом:
Здесь мы видим, что при доступе к атрибуту attribute, являющимся дескриптором, на самом деле вызываются методы дескриптора. Надо также заметить, что дескрипторы вызываются из метода __getattribute__() (который, в свою очередь, имеет смысл только для «новых» классов) определенного в классе object и его переопределение может отменить автоматическое обращение к дескрипторам при доступе к атрибутам. Так же следует знать, что если дескриптор определяет только метод __get__(), то атрибут, за которым стоит такой дескриптор, может быть переопределен присваиванием другого значения атрибута объекту. Если же дополнительно определен метод __set__(), то атрибут объекта не может быть переопределен таким образом.
Примеры дескрипторов
Для примера реализуем аналоги встроенных дескриторов property, classmethod и staticmethod в Python. Дескриптор, имеющий поведение property, может быть представлен следующим классом:
Здесь операции запроса значения атрибута, установки атрибута и его удаления делегируются функциям, переданным в конструктор.
Поведение classmethod можно эмулировать следующим образом:
Здесь первый атрибут при вызове метода заменяется классом объекта.
И наконец staticmethod может быть представлен так:
Менеджеры контекста
Менеджеры контекста — это механизм стоящий за ключевым словом with. Ключевое слово with появилось еще в Python 2.5, но к нему можно было получить доступ только через __future__ импорт: from __future__ import with_statement. Начиная с Python 2.6, ключевое слово with может быть полностью доступно без импортирования из __future__.
Ключевое слово with определяет блоки кода, которые прежде использовали try/finally. Для уверенности в выполнении кода, его заключали в блок finally. with имеет следующую форму:
Здесь «выражение» должно вернуть объект предоставляющий протокол менеджера контекста. Для некоторых встроенных объектов уже определены менеджеры контекста. Например, такой менеджер определен для файлов, чтобы быть уверенным, что файл будет закрыт при выходе из блока:
В простейшем случае такая конструкция эквивалентна следующей:
Протокол менеджера контекста содержит всего два метода: __enter__() и __exit__(). В начале выполнения блока кода вызывается метод __enter__(), который должен вернуть объект присваиваемый переменной, после чего выполняется блок кода. Если блок кода выкидывает исключение, то вызывается метод __exit__() с информацией об исключении. Если выполнение блока завершилось успешно, вся информация об исключении равна None. Пример работы:
Кроме методов, предоставляющих протокол менеджера контекста, здесь также определен вспомогательный метод execute(), который будет представлять код внутри блока:
В случае, если метод __exit__() возвращает «ложь», исключение будет выкинуто за пределы блока. При этом метод __exit__() никогда не должен сам выкидывать полученное исключение, а управлять этим только через возвращаемое значение:
Модуль contextlib
Новый модуль contextlib (появившийся в Python 2.5) предоставляет функции и декораторы, упрощающие создание и работу с менеджерами контекста. На данный момент модуль предоставляет три функции:
contextmanager(функция) — декоратор, упрощающий создание менеджеров контекста. Вместо создания класса, предоставляющего интерфейс менеджера контекста, можно использовать декоратор с функцией-генератором, например:
Теперь мы можем использовать test() как менеджер контекста. Результат yield будет присвоен переменной:
будет эквивалентен коду:
closing(объект) — функция, возвращающая менеджер контекста, который закрывает объект по завершении блока. Например:
В этом примере в конце блока будет вызван метод page.close().
Заключение
В этой статье были рассмотрены достаточно сложные аспекты использования Python, которые вы скорее всего не будете использовать в каждой программе. Но, в то же время, описанный инструментарий может значительно упростить и сделать более гибким сложный код, что позволит взглянуть по-новому на все разрабатываемое приложение в целом. Плюс знание этих инструментов и описанные особенности внутренней работы интерпретатора, должны поднять на новую ступень ваш уровень, как разработчика ПО.