Для начала перечислю эти методы.
__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. При следующих обращениях декоратор просто берет из установленного им атрибута значение и возвращает его запросившей стороне. Немного запутанно, но, я надеюсь, глядя на код вы разберетесь. :) [...]
ОтветитьУдалитьДобрый день, статья понравилась, только не могу понять разницу между аттрибутами класса и аттрибутами объекта, в чем разница, когда объект - это и есть как бы "живое" воплощение класса?
ОтветитьУдалить