Курс по Python ООП: https://stepik.org/a/116336
На этом занятии
мы поговорим о работе с атрибутами класса и его экземплярами. Я напомню, что класс
можно воспринимать как некое пространство имен, в котором записаны свойства и
методы. Например, если вернуться к классу Point (представления
точки на плоскости):
class Point:
MAX_COORD = 100
MIN_COORD = 0
def __init__(self, x, y):
self.x = x
self.y = y
def set_coord(self, x, y):
self.x = x
self.y = y
то здесь мы
видим определение четырех атрибутов: двух свойств MAX_COORD и MIN_COORD и двух методов __init__ и set_coord. Это атрибуты
класса и при создании экземпляров:
pt1 = Point(1, 2)
pt2 = Point(10, 20)
Эти атрибуты
остаются в пространстве имен класса, не копируются в экземпляры. Но из
экземпляров мы можем совершенно спокойно к ним обращаться, так как пространство
имен объектов содержит ссылку на внешнее пространство имен класса. Если
какой-либо атрибут не существует в экземпляре, то поиск переходит во внешнее
пространство, то есть, в класс и поиск продолжается там. Поэтому мы совершенно
спокойно можем через экземпляр обратиться к свойству класса MAX_COORD:
И получается,
что атрибуты и методы класса – это общие данные для всех его экземпляров.
Далее, когда мы
обращаемся к атрибутам класса внутри методов, объявленных в этом классе, то
должны не просто прописать их имена:
def set_coord(self, x, y):
if MIN_COORD <= x <= MAX_COORD:
self.x = x
self.y = y
а явно указать
перед ними ссылку на класс, то есть, на пространство имен. Либо так:
if Point.MIN_COORD <= x <= Point.MAX_COORD:
но лучше через self:
if self.MIN_COORD <= x <= self.MAX_COORD:
Здесь self – это ссылка на
экземпляр класса, из которого метод вызывается, поэтому мы можем через этот
параметр обращаться к атрибутам класса.
Обо всем этом мы
с вами уже говорили, я лишь еще раз повторил эти важные моменты. А теперь один
нюанс, о который спотыкаются многие начинающие программисты. Давайте
предположим, что нам нужен метод, который бы изменял значение атрибута класса MIN_COORD. Пропишем его
как обычный метод:
def set_bound(self, left):
self.MIN_COORD = left
Иногда ошибочно
здесь рассуждают так. Мы обращаемся к атрибуту класса MIN_COORD и присваиваем
ему новое значение left. Те из вас, кто внимательно смотрел предыдущие
занятия, понимают, в чем ошибочность такого рассуждения. Да, когда мы через self (ссылку на
объект) записываем имя атрибута и присваиваем ему какое-либо значение, то
оператор присваивания создает этот атрибут в локальной области видимости, то
есть, в самом объекте. В результате, у нас появляется новое локальное свойство
в экземпляре класса:
pt1.set_bound(-100)
print(pt1.__dict__)
А в самом классе
одноименный атрибут остается без изменений:
Поэтому,
правильнее было бы здесь объявить метод уровня класса и через него менять
значения атрибутов MIN_COORD и MAX_COORD:
@classmethod
def set_bound(cls, left):
cls.MIN_COORD = left
Тогда в самом
объекте не будет создаваться никаких дополнительных свойств, а в классе
изменится значение переменной MIN_COORD, так, как мы
этого и хотели.
Будем полагать,
что мы теперь с вами хорошо понимаем, как обращаться к атрибутам класса и его
объектов. Во второй части занятия я хочу вам рассказать о четырех магических
методах, которые используются при работе с атрибутами:
-
__setattr__(self, key, value)__ – автоматически
вызывается при изменении свойства key класса;
-
__getattribute__(self,
item) – автоматически вызывается при получении свойства класса с именем item;
-
__getattr__(self, item) – автоматически
вызывается при получении несуществующего свойства item класса;
-
__delattr__(self,
item) – автоматически вызывается при удалении свойства item (не важно:
существует оно или нет).
Работают они
достаточно просто. Начнем с метода __getattribute__ и с его помощью ограничим
доступ к приватным свойствам __x и __y экземпляра. Для
простоты я переопределю класс Point, следующим образом:
class Point:
MAX_COORD = 100
MIN_COORD = 0
def __init__(self, x, y):
self.__x = x
self.__y = y
def __getattribute__(self, item):
print("__getattribute__")
return object.__getattribute__(self, item)
Здесь добавлен новый
магический метод __getattribute__. Он автоматически вызывается, когда
идет считывание атрибута через экземпляр класса. Например, при обращении к
свойству MIN_COORD:
Или к приватному
свойству через специальное имя:
Но раз это так,
то давайте явно запретим считывать такой атрибут из экземпляра класса. Для
этого пропишем в методе __getattribute__ проверку:
def __getattribute__(self, item):
if item == "_Point__x":
raise ValueError("Private attribute")
else:
return object.__getattribute__(self, item)
То есть, мы
смотрим, если идет обращение к приватному атрибуту по внешнему имени _Point__x, то генерируем
исключение ValueError. И,
действительно, после запуска программы видим отображение этой ошибки в консоли.
Вот так, через магический метод __getattribute__ можно
реализовывать определенную логику при обращении к атрибутам через экземпляр класса.
Следующий
магический метод __setattr__ автоматически вызывается в момент
присваивания атрибуту нового значения. Пропишем формально этот метод в классе Point:
def __setattr__(self, key, value):
print("__setattr__")
object.__setattr__(self, key, value)
После запуска
видим несколько сообщений «__setattr__». Это связано с тем, что в
момент создания экземпляров класса в инициализаторе __init__ создавались
локальные свойства __x и __y. В этот момент
вызывался данный метод. Также в переопределенном методе __setattr__ мы должны
вызывать соответствующий метод из базового класса object, иначе,
локальные свойства в экземплярах создаваться не будут.
Давайте теперь
для примера через этот магический метод запретим создание локального свойства с
именем z. Сделаем это
следующим образом:
def __setattr__(self, key, value):
if key == 'z':
raise AttributeError("недопустимое имя атрибута")
else:
object.__setattr__(self, key, value)
Обратите
внимание, что внутри метода __setattr__ нельзя менять свойства напрямую:
def __setattr__(self, key, value):
if key == 'z':
raise AttributeError("недопустимое имя атрибута")
else:
self.__x = value
В этом случае
метод __setattr__ начнет
выполняться по рекурсии, пока не возникнет ошибка достижения максимальной
глубины рекурсии. Если нужно сделать что-то подобное, то используйте коллекцию __dict__:
self.__dict__[key] = value
или, если
требуется стандартное поведение метода, то вызывайте его из класса object, как это мы
прописывали вначале:
object.__setattr__(self, key, value)
Следующий
магический метод __getattr__ автоматически вызывается, если идет
обращение к несуществующему атрибуту. Добавим его в наш класс:
def __getattr__(self, item):
print("__getattr__: " + item)
Если ниже
обратиться к несуществующему свойству, например:
то увидим сообщение
«__getattr__: a» и значение None, которое вернул данный метод. Если же
прописать существующий атрибут:
то этот
магический метод уже не вызывается. Зачем он может понадобиться? Например, нам
необходимо определить класс, в котором при обращении к несуществующим атрибутам
возвращается значение False, а не генерируется исключение. Для
этого записывается метод __getattr__ в виде:
def __getattr__(self, item):
return False
Наконец, последний
магический метод __delattr__ вызывается в момент удаления
какого-либо атрибута из экземпляра класса:
def __delattr__(self, item):
print("__delattr__: "+item)
Добавим новое
локальное свойство в экземпляр pt1:
затем выполним
команду его удаления:
и видим, что
действительно был вызван метод __delattr__, правда, сам атрибут удален не
был:
Это из-за того,
что внутри этого метода нужно вызвать соответствующий метод класса object, который и
выполняет непосредственное удаление:
def __delattr__(self, item):
object.__delattr__(self, item)
Я думаю, что из
этого занятия вы хорошо себе уяснили, как происходит обращение к атрибутам
класса и как можно управлять доступом к ним через магические методы.
Курс по Python ООП: https://stepik.org/a/116336