На предыдущем
занятии мы с вами рассмотрели создание объектов-свойств и видели, что для
определения нескольких однотипных таких объектов приходится дублировать
программный код, что не очень хорошо. Чтобы этого избежать можно
воспользоваться механизмом дескрипторов и сейчас вы узнаете что это такое.
В самом простом
случае дескриптор – это класс, в котором определены следующие специальные
методы:
class CoordValue:
def __get__(self, instance, owner):
return self.__value
def __set__(self, instance, value):
self.__value = value
def __delete__(self, obj):
del self.__value
Как вы уже
догадались, get – это геттер, set – сеттер, а delete – вызывается
при удалении свойства дескриптора. И далее, используя этот класс, создадим два
дескриптора в классе Point:
class Point:
coordX = CoordValue()
coordY = CoordValue()
def __init__(self, x = 0, y = 0):
self.coordX = x
self.coordY = y
Сразу скажу, что
это не совсем верная реализация дескрипторов, но я намеренно допустил ошибку,
чтобы вы лучше поняли материал. Если визуально представить эти классы и
экземпляр объекта:
то это будет
выглядеть так. Смотрите, здесь свойства экземпляра coordX и coordY берутся
непосредственно из класса Point. Мы в этом можем убедиться, если
выведем их id:
print( id(pt.coordX), id(Point.coordX) )
И даже операция
в конструкторе класса:
не создает новое
локальное свойство, так как в дескрипторах оператор присваивания перегружен и
идет просто вызов метода __set__ класса CoordValue. При этом
вызове параметр self ссылается на объект coordX, в котором
приватному значению __value присваивается значение value. То же самое и
при обращении к coordY. Только здесь self будет ссылаться
уже на второй объект и информация записывается в другую переменную __value.
И если мы будем
работать с дескрипторами подобно свойствам объекта, то вроде бы у нас все
работает как надо:
pt = Point(1, 2)
print( pt.coordX, pt.coordY )
pt.coordX = 100
print( pt.coordX, pt.coordY )
Но давайте
теперь создадим еще один экземпляр:
pt = Point(1, 2)
pt2 = Point(10, 20)
Смотрите, при
выполнении мы видим, что свойства обоих объектов оказываются идентичными:
print( pt.coordX, pt.coordY )
print( pt2.coordX, pt2.coordY )
И вы уже
догадались почему: coordX, coordY второго
экземпляра также берутся напрямую из класса Point, то есть, это
все те же самые дескрипторы.
Давайте теперь
поправим программу и сделаем так, чтобы координаты точки хранились
непосредственно в экземплярах классов. Перепишем класс дескриптора вот в таком виде:
class CoordValue:
def __init__(self, name):
self.__name = name
def __get__(self, instance, owner):
return instance.__dict__[self.__name]
def __set__(self, instance, value):
instance.__dict__[self.__name] = value
Здесь параметр instance
ссылается на экземпляр класса Point, для которого был вызван метод. И через
него, используя коллекцию __dict__ мы делаем обработку локальных
свойств: возвращаем их или устанавливаем. А, чтобы знать имя локального
свойства, здесь был добавлен конструктор с параметром name – имя свойства.
И мы его сохраняем в экземпляре объекта-дескриптора в приватной переменной __name.
Далее, в самом
классе Point сделаем такие
вызовы:
coordX = CoordValue("coordX")
coordY = CoordValue("coordY")
И, теперь, при
запуске программы, мы видим разные значения координат для разных экземпляров
классов:
pt = Point(1, 2)
pt2 = Point(10, 20)
print( pt.coordX, pt.coordY )
print( pt2.coordX, pt2.coordY )
Но эту программу
можно еще упростить и улучшить. Начиная с версии Python 3.6 и выше, у
дескрипторов появился новый весьма полезный метод:
def __set_name__(self, owner, name):
print(name)
self.__name = name
Он вызывается
автоматически при создании экземпляра дескриптора и в параметре name хранится имя
экземпляра класса. Мы вполне можем его использовать для создания
соответствующих локальных атрибутов в экземплярах классов Point. И конструктор
нам больше не нужно переопределять, уберем его. В самом классе Point создадим
дескрипторы без указания имен:
coordX = CoordValue()
coordY = CoordValue()
и, запуская
программу, видим, имена coordX и coordY и полную
работоспособность наших дескрипторов.
И у вас здесь
может возникнуть вопрос: у нас в экземплярах класса есть локальные атрибуты coordX, coordY и доступ к
дескрипторам класса Point с теми же именами? Как Python «понимает», что
нужно обращаться именно к дескрипторам, а не к локальным свойствам объекта pt? Все дело в
приоритетах. Если в самом классе (или базовом классе) имеется дескриптор с тем
же именем, что и свойство в коллекции __dict__, то приоритет
отдается дескриптору и именно он берется для записи или считывания значений. Поэтому
наша программа корректно работала.
Но здесь тоже
есть одна тонкость: дескрипторы в Python бывают двух
видов: для данных и для не данных (non-data descriptor). Последний отличается тем, что реализует только один метод __get__:
class NoDataDescr:
def __set_name__(self, owner, name):
self.__name = name
def __get__(self, instance, owner):
return "NoDataDescr __get__"
И, если его
создать в классе Point:
то при запуске
мы увидим, строчку из этого «дескриптора не данных». Но, если выполним
присвоение свойству с тем же именем:
то наш
дескриптор noData пропадет из
локального окружения экземпляра pt. Что, в общем то не удивительно, т.к. в
этом дескрипторе нет метода set для изменения
соответствующего локального свойства.
Но, чаще всего
на практике используют дескрипторы для данных, то есть, и для записи и для
считывания значений. Поэтому этот последний момент просто нужно знать, чтобы избежать
лишних ошибок.
И в заключение
отмечу такую вещь. Дескрипторы – это безусловно классный, красивый и гибкий
механизм, позволяющий создавать красивый программный код. Однако, прежде чем
использовать какой-либо инструмент, задайте себе вопрос: действительно ли он
здесь нужен, не проще ли все реализовать двумя методами: геттером и сеттером и,
как говорится, не городить огород? Неоправданное усложнение программы это также
плохо как и отказ от существующих инструментов языка программирования. При
проектировании программ следует придерживаться грани, когда с одной стороны
простые способы реализации, а с другой – возможные проблемы при масштабировании
проекта. Но, это все приходит с опытом и, так как дорогу осилит только идущий,
то вперед на покорение дескрипторов и свойств классов. Для начала попробуйте
реализовать выполнить следующие задания на Python.
Задания для самоподготовки
1. Объявите
класс Calendar для хранения
даты: день, месяц, год. Определите свойства для записи и считывания этой
информации из этого класса. (Дополнение: используя __slots__ разрешите
использовать только строго определенные локальные свойства в экземплярах
класса).
2. Объявите
класс Rectangle, хранящий
координаты верхней левой и правой нижней точек. Создайте дескрипторы для записи
и считывания этих значений в классе (атрибуты с данными координат должны быть
приватными).