Магические методы __setattr__, __getattribute__, __getattr__ и __delattr__
На этом занятии мы поговорим о работе с атрибутами класса и его экземплярами. Я напомню, что класс можно воспринимать как некое пространство имен, в котором записаны свойства и методы. Например, если вернуться к классу 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:
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 (не важно: существует оно или нет).
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)
def __setattr__(self, key, value): if key == 'z': raise AttributeError("недопустимое имя атрибута") else: self.__x = value
В этом случае метод __setattr__ начнет выполняться по рекурсии, пока не возникнет ошибка достижения максимальной глубины рекурсии. Если нужно сделать что-то подобное, то используйте коллекцию __dict__:
или, если требуется стандартное поведение метода, то вызывайте его из класса 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)
Это из-за того, что внутри этого метода нужно вызвать соответствующий метод класса object, который и выполняет непосредственное удаление:
def __delattr__(self, item): object.__delattr__(self, item)