четверг, 2 ноября 2006 г.

Дескрипторы в Python

Попробую дать определение, что такое классы-дескрипторы. Классы-дескрипторы — это классы "нового стиля" (new-style classes), которые определяют один или несколько специальных методов, перечисленных ниже. Эти методы переопределяют процедуру доступа к атрибуту класса через класс или экземпляр класса (класс тоже нового стиля) в том случае, если в качестве этого атрибута выступает экземпляр класса-дескриптора. Кто понял это с первого раза, тому пирожок.

Для начала перечислю эти методы.

__get__(self, instance, owner)

Переопределяет процедуру чтения атрибута. Вызывается при обращении к атрибуту класса (аргумент owner) или или экземпляра класса (аргумент instance). Должен возвращать значение, которое и будет отдано вызывающему коду или вызывать исключение AttributeError. Аргумент instance содержит тот объект, к атрибуту которого мы обратились. В случае, когда обращение происходит к атрибуту класса, а не экземпляра класса, аргумент instance содержит значениеNone. Аргумент owner всегда содержит класс, вне зависимости от того, через класс или через экземпляр класса мы обратились к атрибуту.

__set__(self, instance, value)

Переопределяет процедуру записи значения атрибута объекта instance.

__delete__(self, instance)

Переопределяет процедуру удаления атрибута объекта instance



Для того, чтобы понять, зачем это и как это работает, рассмотрим пример.

[python]
class FooDescriptor(object):
value = 1

def __get__(self, instance, owner):
print "FooDescriptor.__get__ called"
return self.value

def __set__(self, instance, value):
print "FooDescriptor.__set__ called with value %d" % value
self.value = value


class Bar(object):
foo = FooDescriptor()

bar = Bar()
print bar.foo
bar.foo = 2
print bar.foo
[/python]

Мы определили класс FooDescriptor, который определяет методы __get__ и __set__, а также класс Bar, в котором атрибуту foo присвоили экземпляр класса FooDescriptor. Запустим наш пример.


FooDescriptor.__get__ called
1
FooDescriptor.__set__ called with value 2
FooDescriptor.__get__ called
2


Вывод свидетельствует о том, что при каждом обращении к bar.foo, как на чтение так и на запись, вызываются методы __get__ и __set__ класса FooDescriptor. Фактически, мы переопределили стандартную обработку обращения к атрибуту объекта изнутри того класса, экземпляр которого был присвоен этому атрибуту! Как видно из кода, класс Bar не делает ничего для того, чтобы переопределить доступ к своим атрибутам.

Что происходит на самом деле?
При вызове bar.foo неявно вызывается метод __getattribute__ класса Bar (фактически, этот метод унаследован от базового класса type). Этот метод преобразует обращение к bar.foo в конструкцию, которую на python можно описать так: Bar.__dict__['foo'].__get__(bar, Bar). Само собой, такой вызов происходит только если:

  1. класс объекта, на который ссылается bar.foo, определяет метод __get__;

  2. метод __getattribute__ класса Bar не переопределен;

  3. важно: атрибут foo определен как атрибут класса Bar, а не атрибут его экземпляра bar. Как мы видим из приведенного выше выражения, обращение идет через атрибут класса. Если хочется присвоить дескриптор атрибуту объекта, то это надо делать через класс, например, в методе __init__ класса Bar написать следующее: self.__class__.foo = FooDescriptor()



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

[python]
class Obj(object):
__x = 0

def getX(self):
return self.__x
def setX(self, value):
self.__x = value
def delX(self):
del self.__x

x = property(getX, setX, delX)
[/python]

В случае обращения к атрибуту x будет вызван соответствующий метод для возврата, установки и удаления атрибута __x. property — это класс-дескриптор, определяющий методы __get__, __set__ и __delete__, и вызывающий в нихсоответствующие методы, переданные конструктору property. В результате выражения x = property(getX, setX, delX) атрибуту x присваивается значение экземпляра класса property. Все просто, не так ли?

Еще чаще механизм дескрипторов употребляется при определении классов, точнее — методов классов. Методы класса — это не простые функции, а экземпляры класса-дескриптора instancemethod. При вызове метода класса или объекта срабатывает метод __get__ класса instancemethod, который вызывает исходную функцию, послужившую при создании метода, и передает ей в качестве первого аргумента объект, в котором определен метод.

В методы дескриптора передается класс и экземпляр класса — это позволяет гибко настраивать поведение объекта-атрибута в зависимости от того, в каком контексте он был вызван. Рассмотрим следующий пример. У нас есть класс-контейнер для данных DataDescriptor, и мы хотим управлять процессом вывода этих данных. Допустим, у нас есть класс Widget, и унаследованные от него классы TextWidget и HTMLWidget, служащие для вывода данных в текстовом и HTML-виде соответственно. Мы отдаем данные в текстовом и HTML-форматах, если обращение к объекту происходит через атрибут этих классов, и сам объект во всех остальных случаях (поведение по умолчанию, если метод __get__ не определен).

[python]
class DataDescriptor(object):
data = "test"

def __get__(self, instance, owner):
if isinstance(instance, TextWidget):
return self.data
elif isinstance(instance, HTMLWidget):
return "
%s
" % self.data
else:
return self

class Widget(object):
data = DataDescriptor()

class TextWidget(Widget):
pass

class HTMLWidget(Widget):
pass

widget = Widget()
text = TextWidget()
html = HTMLWidget()

print widget.data
print text.data
print html.data
[/python]

При запуске примера получится примерно такой вывод:



test
<div>test</div>


Надеюсь, сейчас стало все понятно. Можете брать свой пирожок :)

Ссылки:

7 комментариев:

  1. Хочется в дополнение еще заметить, что экземпляры дескриптора присваиваются именно классу, а не экземпляру. То есть здесь:

    class Bar(object):
    foo = FooDescriptor()

    именно `foo`, а не `self.foo`. Помнится, для меня это было неочевидным, когда я про дескрипторы узнал.

    ОтветитьУдалить
  2. Почему неочевидно?
    Это единственная нормальная конструкция объявления атрибутов класса.
    Другое дело, что нельзя присваивать self.foo в методе объекта (т.е. в рантайме, атрибуту объекта) - дескриптор не будет работать, т.к. он работает только тогда, когда значение присвоено атрибуту класса. Эту особенность я отметил.

    ОтветитьУдалить
  3. Интересно, но пример с DataDescriptor не убедителен. Я бы такой неочевидный дизайн использовать в данном случае поостерегся.

    ОтветитьУдалить
  4. Да, как решение дизайна пример не очень полезный. Цель была показать как оно работает на простом примере.

    ОтветитьУдалить
  5. [...] Итак, ссылочные поля добавляют в модели, которые связывают, атрибуты для доступа в обе стороны: от родительских объектов к детям и наоборот. Эти атрибуты &#8212; питоновские дескрипторы &#8212; такие интересные объекты, которые позволяют определить операции чтения их значений и установки им новых значений (делфисты узнают в этом объектные property). [...]

    ОтветитьУдалить
  6. [...] Декоратор работает очень просто, он оборачивает указанный метод, в нашем случае это lazy_attr .При этом обернутый метод становится свойством (property) объекта. Далее, при первом обращении к свойству, декоратор устанавливает приватный атрибут вида &#8220;__имя_свойства&#8221;, у нас это будет атрибут self.__lazy_attr. Значением которого является результат вызова обернутого метода и возвращает это значение. Обернутый нами метод lazy_attr вернет экземпляр VeryBigObject. При следующих обращениях декоратор просто берет из установленного им атрибута значение и возвращает его запросившей стороне. Немного запутанно, но, я надеюсь, глядя на код вы разберетесь. :) [...]

    ОтветитьУдалить
  7. Добрый день, статья понравилась, только не могу понять разницу между аттрибутами класса и аттрибутами объекта, в чем разница, когда объект - это и есть как бы "живое" воплощение класса?

    ОтветитьУдалить

Постоянные читатели