Для начала перечислю эти методы.
__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)
. Само собой, такой вызов происходит только если:- класс объекта, на который ссылается
bar.foo
, определяет метод__get__
; - метод
__getattribute__
классаBar
не переопределен; - важно: атрибут
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.dataelse:
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>
Надеюсь, сейчас стало все понятно. Можете брать свой пирожок :)
Ссылки:
Хочется в дополнение еще заметить, что экземпляры дескриптора присваиваются именно классу, а не экземпляру. То есть здесь:
ОтветитьУдалитьclass Bar(object):
foo = FooDescriptor()
именно `foo`, а не `self.foo`. Помнится, для меня это было неочевидным, когда я про дескрипторы узнал.
Почему неочевидно?
ОтветитьУдалитьЭто единственная нормальная конструкция объявления атрибутов класса.
Другое дело, что нельзя присваивать self.foo в методе объекта (т.е. в рантайме, атрибуту объекта) - дескриптор не будет работать, т.к. он работает только тогда, когда значение присвоено атрибуту класса. Эту особенность я отметил.
Интересно, но пример с DataDescriptor не убедителен. Я бы такой неочевидный дизайн использовать в данном случае поостерегся.
ОтветитьУдалитьДа, как решение дизайна пример не очень полезный. Цель была показать как оно работает на простом примере.
ОтветитьУдалить[...] Итак, ссылочные поля добавляют в модели, которые связывают, атрибуты для доступа в обе стороны: от родительских объектов к детям и наоборот. Эти атрибуты — питоновские дескрипторы — такие интересные объекты, которые позволяют определить операции чтения их значений и установки им новых значений (делфисты узнают в этом объектные property). [...]
ОтветитьУдалить[...] Декоратор работает очень просто, он оборачивает указанный метод, в нашем случае это lazy_attr .При этом обернутый метод становится свойством (property) объекта. Далее, при первом обращении к свойству, декоратор устанавливает приватный атрибут вида “__имя_свойства”, у нас это будет атрибут self.__lazy_attr. Значением которого является результат вызова обернутого метода и возвращает это значение. Обернутый нами метод lazy_attr вернет экземпляр VeryBigObject. При следующих обращениях декоратор просто берет из установленного им атрибута значение и возвращает его запросившей стороне. Немного запутанно, но, я надеюсь, глядя на код вы разберетесь. :) [...]
ОтветитьУдалитьДобрый день, статья понравилась, только не могу понять разницу между аттрибутами класса и аттрибутами объекта, в чем разница, когда объект - это и есть как бы "живое" воплощение класса?
ОтветитьУдалить