Классы и объекты. Атрибуты классов и объектов
Меня зовут Сергей Балакирев и на этом занятии мы с вами узнаем, как в Python определять классы, создавать объекты (экземпляры) этих классов, а также добавлять и удалять их атрибуты (то есть, данные).
Предположим, мы хотим определить класс для хранения координат точек на плоскости. Для начала я его запишу без какого-либо содержимого, только имя класса Point и все:
class Point: pass
Здесь оператор pass указывает, что мы в классе ничего не определяем. Также обратите внимание, что в соответствии со стандартом PEP8 имя класса принято записывать с заглавной буквы. И, конечно же, называть так, чтобы оно отражало суть этого класса. В дальнейшем я буду придерживаться этого правила.
Итак, у нас получилось простейшее определение класса с именем Point. Но в таком виде он не особо полезен. Поэтому я пропишу в нем два атрибута: color – цвет точек; circle – радиус точек:
class Point: color = 'red' circle = 2
Обратите внимание, переменные внутри класса обычно называются атрибутами класса или его свойствами. Я буду в дальнейшем использовать эту терминологию. Теперь в нашем классе есть два атрибута color и circle. Но, как правильно воспринимать эту конструкцию? Фактически, сам класс образует пространство имен, в данном случае с именем Point, в котором находятся две переменные color и circle. И мы можем обращаться к ним, используя синтаксис для пространства имен, например:
Point.color = 'black'
или для считывания значения:
Point.circle
(В консольном режиме увидим значение 2). А чтобы увидеть все атрибуты класса можно обратиться к специальной коллекции __dict__:
Point.__dict__
Здесь отображается множество служебных встроенных атрибутов и среди них есть два наших: color и circle.
Теперь сделаем следующий шаг и создадим экземпляры этого класса. В нашем случае для создания объекта класса Point достаточно после его имени прописать круглые скобки:
a = Point()
Смотрите, справа на панели в Python Console у нас появилась переменная a, через которую доступны два атрибута класса: color и circle.
Давайте создадим еще один объект этого класса:
b = Point()
Появилась переменная b, которая ссылается на новый объект (он расположен по другому адресу) и в этом объекте мы также видим два атрибута класса Point. По аналогии можно создавать произвольное количество экземпляров класса.
С помощью функции type мы можем посмотреть тип данных для переменных a или b:
type(a)
Видим, что это класс Point. Эту принадлежность можно проверить, например, так:
type(a) == Point
isinstance(a, Point)
То есть, имя класса здесь выступает в качестве типа данных. Но давайте детальнее разберемся, что же у нас в действительности получилось?
Во-первых, объекты a и b образуют свое пространство имен – пространство имен экземпляров класса и, во-вторых, не содержат никаких собственных атрибутов. Свойства color и circle принадлежат непосредственно классу Point и находятся в нем, а объекты a и b лишь имеют ссылки на эти атрибуты класса. Поэтому я не случайно называю их именно атрибутами класса, подчеркивая этот факт. То есть, атрибуты класса – общие для всех его экземпляров. И мы можем легко в этом убедиться. Давайте изменим значение свойства circle на 1:
Point.circle = 1
И в обоих объектах это свойство стало равно 1. Мало того, если посмотреть коллекцию __dict__ у объектов:
a.__dict__
то она будет пустой, так как в наших экземплярах отсутствуют какие-либо атрибуты. Но, тем не менее, мы можем через них обращаться к атрибутам класса:
a.color b.circle
Но, если мы выполним присваивание, например:
a.color = 'green'
То, смотрите, в объекте a свойство color стало ‘green’, а в b – прежнее. Почему? Дело в том, что мы здесь через переменную a обращаемся к пространству имен уже экземпляра класса и оператор присваивания в Python создает новую переменную, если она отсутствует в текущей локальной области видимости, то есть, создается атрибут color уже непосредственно в объекте a:

Мы можем в этом убедиться, если отобразим коллекцию __dict__ этого объекта:
a.__dict__
То есть, мы с вами создали локальное свойство в объекте a. Этот момент нужно очень хорошо знать и понимать. На этом принципе в Python построено формирование атрибутов классов и локальных атрибутов их экземпляров.
Добавление и удаление атрибутов класса
Кстати, по аналогии, мы можем создавать новые атрибуты и в классе, например, так:
Point.type_pt = 'disc'
Или то же самое можно сделать с помощью специальной функции:
setattr(Point, 'prop', 1)
Она создает новый атрибут в указанном пространстве имен (в данном случае в классе Point) с заданным значением. Если эту функцию применить к уже существующему атрибуту:
setattr(Point, 'type_pt', 'square')
то оно будет изменено на новое значение.
Если же мы хотим прочитать какое-либо значение атрибута, то достаточно обратиться к нему. В консольном режиме это выглядит так:
Point.circle
Но, при обращении к несуществующему атрибуту класса, например:
Point.a
возникнет ошибка. Этого можно избежать, если воспользоваться специальной встроенной функцией:
getattr(Point, 'a', False)
Здесь третий аргумент – возвращаемое значение, если атрибут не будет найден. Эту же функцию можно вызвать и с двумя аргументами:
getattr(Point, 'a')
Но тогда также будет сгенерирована ошибка при отсутствии указанного атрибута. Иначе:
getattr(Point, 'color')
она возвратит его значение. То есть, эта функция дает нам больше гибкости при обращении к атрибутам класса. Хотя на практике ей пользуются только в том случае, если есть опасность обращения к несуществующим атрибутам. Обычно, все же, применяют обычный синтаксис:
Point.color
Наконец, мы можем удалять любые атрибуты из класса. Сделать это можно, по крайней мере, двумя способами. Первый – это воспользоваться оператором del:
del Point.prop
Если повторить эту команду и попытаться удалить несуществующий атрибут, возникнет ошибка. Поэтому перед удалением рекомендуется проверять существование удаляемого свойства. Делается это с помощью функции hasattr:
hasattr(Point, 'prop')
Она возвращает True, если атрибут найден и False – в противном случае.
Также удалить атрибут можно с помощью функции:
delattr(Point, 'type_pt')
Она работает аналогично оператору del.
И, обратите внимание, удаление атрибутов выполняется только в текущем пространстве имен. Например, если попытаться удалить свойство color из объекта b:
del b.color
то получим ошибку, т.к. в объекте b не своих локальных свойств и удалять здесь в общем то нечего. А вот в объекте a есть свое свойство color, которое мы с вами добавляли:
a.__dict__
и его можно удалить:
del a.color
Смотрите, после удаления локального свойства color в объекте a становится доступным атрибут color класса Point с другим значение ‘black’. И это логично, т.к. если свойство не обнаруживается в локальной области, то поиск продолжается в следующей (внешней) области видимости. А это (для объекта a) класс Point. Вот этот момент также следует хорошо понимать при работе с локальными свойствами объектов и атрибутами класса.
Атрибуты экземпляров классов
Теперь, когда мы знаем, как создаются атрибуты, вернемся к нашей задаче формирования объектов точек на плоскости. Мы полагаем, что атрибуты color и circle класса Point – это общие данные для всех объектов этого класса. А вот координаты точек должны принадлежать его экземплярам. Поэтому для объектов a и b мы определим локальные свойства x и y:
a.x = 1 a.y = 2 b.x = 10 b.y = 20
То есть, свойства x, y будут существовать непосредственно в объектах, но не в самом классе Point:

В результате, каждый объект представляет точку с независимыми координатами на плоскости. А цвет и их размер – общие данные для всех объектов.
В заключение этого занятия отмечу, что в любом классе языка Python мы можем прописывать его описание в виде начальной строки, например, так:
class Point: "Класс для представления координат точек на плоскости" color = 'red' circle = 2
В результате, специальная переменная:
Point.__doc__
будет ссылаться на это описание. Обычно, при создании больших программ, в ключевых классах создают такие описания, чтобы в последующем было удобнее возвращаться к ранее написанному коду, корректировать его и использовать, не обращаясь к специальной документации.
Заключение
Итак, из этого занятия вы должны себе хорошо представлять, как определяются классы в Python и создаются объекты класса. Что из себя представляют атрибуты класса и атрибуты объектов, как они связаны между собой. Уметь обращаться к этим атрибутам, добавлять, удалять их, а также проверять существование конкретного свойства в классе или объекте класса.
Видео по теме

Концепция ООП простыми словами

#1. Классы и объекты. Атрибуты классов и объектов

#2. Методы классов. Параметр self

#3. Инициализатор __init__ и финализатор __del__

#4. Магический метод __new__. Пример паттерна Singleton

#5. Методы класса (classmethod) и статические методы (staticmethod)

#6. Режимы доступа public, private, protected. Сеттеры и геттеры

#7. Магические методы __setattr__, __getattribute__, __getattr__ и __delattr__

#8. Паттерн Моносостояние

#9. Свойства property. Декоратор @property

#10. Пример использования объектов property

#11. Дескрипторы (data descriptor и non-data descriptor)

#12. Магический метод __call__. Функторы и классы-декораторы

#13. Магические методы __str__, __repr__, __len__, __abs__

#14 Магические методы __add__, __sub__, __mul__, __truediv__

#15. Методы сравнений __eq__, __ne__, __lt__, __gt__ и другие

#16. Магические методы __eq__ и __hash__

#17. Магический метод __bool__ определения правдивости объектов

#18. Магические методы __getitem__, __setitem__ и __delitem__

#19. Магические методы __iter__ и __next__

#20. Наследование в объектно-ориентированном программировании

#21. Функция issubclass(). Наследование от встроенных типов и от object

#22. Наследование. Функция super() и делегирование

#23. Наследование. Атрибуты private и protected

#24. Полиморфизм и абстрактные методы
#25. Множественное наследование

#26. Коллекция __slots__

#27. Как работает __slots__ с property и при наследовании

#28. Введение в обработку исключений. Блоки try / except

#29. Обработка исключений. Блоки finally и else

#30. Распространение исключений (propagation exceptions)

#31. Инструкция raise и пользовательские исключения

#32. Менеджеры контекстов. Оператор with

#33. Вложенные классы

#34. Метаклассы. Объект type

#35. Пользовательские метаклассы. Параметр metaclass

#36. Метаклассы в API ORM Django

#37. Введение в Python Data Classes (часть 1)

#38. Введение в Python Data Classes (часть 2)

#39. Python Data Classes при наследовании
© 2023 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
Как работать с атрибутами классов и экземпляров в Python
Изучите атрибуты классов и экземпляров в Python, чтобы стать мастером объектно-ориентированного программирования!
Алексей Кодов
Автор статьи
10 июля 2023 в 17:48
Атрибуты классов и экземпляров являются важной частью объектно-ориентированного программирования (ООП) в Python. В этой статье мы разберем, как использовать и работать с атрибутами классов и экземпляров.
Атрибуты классов
Атрибуты классов определяются внутри класса и являются общими для всех объектов этого класса. Они могут быть использованы для хранения констант или общих настроек.
class MyClass: attribute = "value" print(MyClass.attribute) # Вывод: "value"
Атрибуты экземпляров
Атрибуты экземпляра определяются для каждого объекта класса и обычно используются для хранения состояния объекта. Они определяются внутри метода __init__ .
class MyClass: def __init__(self): self.attribute = "value" obj = MyClass() print(obj.attribute) # Вывод: "value"
Работа с атрибутами
Для доступа к атрибутам класса или экземпляра используйте точечную нотацию:
class MyClass: class_attribute = "class value" def __init__(self): self.instance_attribute = "instance value" obj = MyClass() print(MyClass.class_attribute) # Вывод: "class value" print(obj.instance_attribute) # Вывод: "instance value"
Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT

Если вам нужно изменить значение атрибута, просто присвойте ему новое значение:
class MyClass: class_attribute = "class value" def __init__(self): self.instance_attribute = "instance value" obj = MyClass() obj.instance_attribute = "new instance value" MyClass.class_attribute = "new class value" print(MyClass.class_attribute) # Вывод: "new class value" print(obj.instance_attribute) # Вывод: "new instance value"
Однако изменять атрибуты класса через экземпляр класса может привести к неочевидным результатам:
class MyClass: class_attribute = "class value" def __init__(self): self.instance_attribute = "instance value" obj1 = MyClass() obj2 = MyClass() obj1.class_attribute = "new class value" print(MyClass.class_attribute) # Вывод: "class value" print(obj1.class_attribute) # Вывод: "new class value" print(obj2.class_attribute) # Вывод: "class value"
Здесь изменение class_attribute через экземпляр obj1 создало новый атрибут экземпляра с тем же именем, вместо изменения атрибута класса.
Вывод
В этой статье мы рассмотрели основы работы с атрибутами классов и экземпляров в Python. Эти принципы помогут вам понять и использовать ООП в Python для создания структурированных и модульных программ. Удачи в изучении Python!
Работа с атрибутами объекта в Python
В работе с объектно-ориентированным программированием в Python возникают ситуации, когда необходимо получить список всех атрибутов, которые присутствуют у определенного объекта. Для примера, рассмотрим ситуацию, когда создается класс с некоторыми атрибутами:
class MyClass(): def __init__(self, number): self.multi = int(number) * 2 self.str = str(number) object = MyClass(2)
В данном случае, у объекта object класса MyClass есть два атрибута: multi и str . Теперь необходимо получить имена этих атрибутов.
Python предоставляет встроенную функцию dir() , которая возвращает список имён, определённых в текущей области видимости. Если ей передать в качестве аргумента объект, она вернёт список его атрибутов. Пример использования:
print(dir(object))
Но стоит отметить, что вместе с атрибутами объекта, dir() возвращает также и все методы этого объекта, включая служебные методы, которые начинаются и заканчиваются двойным подчеркиванием.
Если необходимо получить только пользовательские атрибуты, то можно воспользоваться функцией vars() . Она возвращает словарь, где ключами являются имена атрибутов, а значениями — их значения. Пример использования:
print(vars(object))
В результате будет получен словарь вида: . Чтобы получить только имена атрибутов, достаточно обратиться к ключам словаря:
print(vars(object).keys())
В результате будет получен список имен атрибутов: [‘multi’, ‘str’] .
Таким образом, Python предоставляет удобные инструменты для работы с атрибутами объектов, позволяющие упростить и ускорить написание кода.
Пользовательские атрибуты в Python

Вы когда нибудь задумывались о том, что происходит, когда вы ставите точку в python? Что скрывает за собой символ str(“\u002E”)? Какие тайны он хранит? Если без мистики, вы знаете как происходит поиск и установка значений пользовательских атрибутов в python? Хотели бы узнать? Тогда… добро пожаловать!
Чтобы время, проведённое за чтением прошло легко, приятно и с пользой, было бы неплохо знать несколько базовых понятий языка. В частности, понимание type и object будут исключительно полезны, так же как знание нескольких примеров обеих сущностей. Почитать о них можно, в том числе, здесь.
Немного о терминологии, которую я использую, прежде чем мы приступим к тому, ради чего собрались:
- Объект есть любая сущность в python (функция, число, строка… словом, всё).
- Класс это объект, чьим типом является type (тип можно подсмотреть в атрибуте __class__).
- Экземпляр некоторого класса A — это объект, у которого в атрибуте __class__ есть ссылка на класс A.
Ах, да, все примеры в статье написаны на python3! Это определённо следует учесть.
Если ничто из вышесказанного не смогло умерить ваше желание узнать, что там будет дальше, приступим!
__dict__
- Сам объект (o.__dict__ и его системные атрибуты).
- Класс объекта (o.__class__.__dict__). Только __dict__ класса, не системные атрибуты.
- Классы, от которых унасаледован класс объекта (o.__class__.__bases__.__dict__).
class StuffHolder: stuff = "class stuff" a = StuffHolder() b = StuffHolder() a.stuff # "class stuff" b.stuff # "class stuff" b.b_stuff = "b stuff" b.b_stuff # "b stuff" a.b_stuff # AttributeError
В примере описан класс StuffHolder с одним атрибутом stuff, который, наследуют оба его экземпляра. Добавление объекту b атрибута b_stuff, никак не отражается на a.
Посмотрим на __dict__ всех действующих лиц:
StuffHolder.__dict__ # a.__dict__ # <> b.__dict__ # a.__class__ # b.__class__ #
(У класса StuffHolder в __dict__ хранится объект класса dict_proxy с кучей разного барахла, на которое пока не нужно обращать внимание).
Ни у a ни у b в __dict__ нет атрибута stuff, не найдя его там, механизм поиска ищет его в __dict__ класса (StuffHolder), успешно находит и возвращает значение, присвоенное ему в классе. Ссылка на класс хранится в атрибуте __class__ объекта.
Поиск атрибута происходит во время выполнения, так что даже после создания экземпляров, все изменения в __dict__ класса отразятся в них:
a.new_stuff # AttributeError b.new_stuff # AttributeError StuffHolder.new_stuff = "new" StuffHolder.__dict__ # a.new_stuff # "new" b.new_stuff # "new"
В случае присваивания значения атрибуту экземпляра, изменяется только __dict__ экземпляра, то есть значение в __dict__ класса остаётся неизменным (в случае, если значением атрибута класса не является data descriptor):
StuffHolder.__dict__ # c = StuffHolder() c.__dict__ # <> c.stuff = "more c stuff" c.__dict__ # StuffHolder.__dict__ #
Если имена атрибутов в классе и экземпляре совпадают, интерпретатор при поиске значения выдаст значение экземпляра (в случае, если значением атрибута класса не является data descriptor):
StuffHolder.__dict__ # d = StuffHolder() d.stuff # "class stuff" d.stuff = "d stuff" d.stuff # "d stuff"
По большому счёту это всё, что можно сказать про __dict__. Это хранилище атрибутов, определённых пользователем. Поиск в нём производится во время выполнения и при поиске учитывается __dict__ класса объекта и базовых классов. Также важно знать, что есть несколько способов переопределить это поведение. Одним из них является великий и могучий Дескриптор!
Дескрипторы
С простыми типами в качестве значений атрибутов пока всё ясно. Посмотрим, как ведёт себя функция в тех же условиях:
class FuncHolder: def func(self): pass fh = FuncHolder() FuncHolder.func # FuncHolder.__dict__ # <. 'func': . > fh.func # >
WTF!? Спросите вы… возможно. Я бы спросил. Чем функция в этом случае отличается от того, что мы уже видели? Ответ прост: методом __get__.
FuncHolder.func.__class__.__get__ #
Этот метод переопределяет механизм получения значения атрибута func экземпляра fh, а объект, который реализует этот метод непереводимо называется non-data descriptor.
Дескриптор — это объект, доступ к которому через атрибут переопределён методами в дескриптор протоколе:
descr.__get__(self, obj, type=None) --> value (переопределяет способ получения значения атрибута) descr.__set__(self, obj, value) --> None (переопределяет способ присваивания значения атрибуту) descr.__delete__(self, obj) --> None (переопределяет способ удаления атрибута)
- Data Descriptor (дескриптор данных) — объект, который реализует метод __get__() и __set__()
- Non-data Descriptor (дескриптор не данных?) — объект, который реализует метод __get__()
Дескрипторы данных
Рассмотрим повнимательней дескриптор данных:
class DataDesc: def __get__(self, obj, cls): print("Trying to access from class ".format(obj, cls)) def __set__(self, obj, val): print("Trying to set for ".format(val, obj)) def __delete__(self, obj): print("Trying to delete from ".format(obj)) class DataHolder: data = DataDesc() d = DataHolder() DataHolder.data # Trying to access from None class d.data # Trying to access from class d.data = 1 # Trying to set 1 for del(d.data) # Trying to delete from
Стоит обратить внимание, что вызов DataHolder.data передаёт в метод __get__ None вместо экземпляра класса.
Проверим утверждение о том, что у дата дескрипторов преимущество перед записями в __dict__ экземпляра:
d.__dict__["data"] = "override!" d.__dict__ # d.data # Trying to access from class
Так и есть, запись в __dict__ экземпляра игнорируется, если в __dict__ класса экземпляра (или его базового класса) существует запись с тем же именем и значением — дескриптором данных.
Ещё один важный момент. Если изменить значение атрибута с дескриптором через класс, никаких методов дескриптора вызвано не будет, значение изменится в __dict__ класса как если бы это был обычный атрибут:
DataHolder.__dict__ # . > DataHolder.data = "kick descriptor out" DataHolder.__dict__ # DataHolder.data # "kick descriptor out"
Дескрипторы не данных
Пример дескриптора не данных:
class NonDataDesc: def __get__(self, obj, cls): print("Trying to access from class ".format(obj, cls)) class NonDataHolder: non_data = NonDataDesc() n = NonDataHolder() NonDataHolder.non_data # Trying to access from None class n.non_data # Trying to access from class n.non_data = 1 n.non_data # 1 n.__dict__ #
Его поведение слегка отличается от того, что вытворял дата-дескриптор. При попытке присвоить значение атрибуту non_data, оно записалось в __dict__ экземпляра, скрыв таким образом дескриптор, который хранится в __dict__ класса.
Примеры использования
Дескрипторы это мощный инструмент, позволяющий контролировать доступ к атрибутам экземпляра класса. Один из примеров их использования — функции, при вызове через экземпляр они становятся методами (см. пример выше). Также распространённый способ применения дескрипторов — создание свойства (property). Под свойством я подразумеваю некое значение, характеризующее состояние объекта, доступ к которому управляется с помощью специальных методов (геттеров, сеттеров). Создать свойство просто с помощью дескриптора:
class Descriptor: def __get__(self, obj, type): print("getter used") def __set__(self, obj, val): print("setter used") def __delete__(self, obj): print("deleter used") class MyClass: prop = Descriptor()
Или можно воспользоваться встроенным классом property, он представляет собой дескриптор данных. Код, представленный выше можно переписать следующим образом:
class MyClass: def _getter(self): print("getter used") def _setter(self, val): print("setter used") def _deleter(self): print("deleter used") prop = property(_getter, _setter, _deleter, "doc string")
В обоих случаях мы получим одинаковое поведение:
m = MyClass() m.prop # getter used m.prop = 1 # setter used del(m.prop) # deleter used
Важно знать, что property всегда является дескриптором данных. Если в его конструктор не передать какую либо из функций (геттер, сеттер или делитер), при попытке выполнить над атрибутом соответствующее действие — выкинется AttributeError.
class MySecondClass: prop = property() m2 = MySecondClass() m2.prop # AttributeError: unreadable attribute m2.prop = 1 # AttributeError: can't set attribute del(m2) # AttributeError: can't delete attribute
- staticmethod — то же, что функция вне класса, в неё не передаётся экземпляр в качестве первого аргумента.
- classmethod — то же, что метод класса, только в качестве первого аргумента передаётся класс экземпляра.
class StaticAndClassMethodHolder: def _method(*args): print("_method called with ", args) static = staticmethod(_method) cls = classmethod(_method) s = StaticAndClassMethodHolder() s._method() # _method called with (,) s.static() # _method called with () s.cls() # _method called with (,)
__getattr__(), __setattr__(), __delattr__() и __getattribute__()
Если нужно определить поведение какого-либо объекта как атрибута, следует использовать дескрипторы (например property). Тоже справедливо для семейства объектов (например функций). Ещё один способ повлиять на доступ к атрибутам: методы __getattr__(), __setattr__(), __delattr__() и __getattribute__(). В отличие от дескрипторов их следует определять для объекта, содержащего атрибуты и вызываются они при доступе к любому атрибуту этого объекта.
__getattr__(self, name) будет вызван в случае, если запрашиваемый атрибут не найден обычным механизмом (в __dict__ экземпляра, класса и т.д.):
class SmartyPants: def __getattr__(self, attr): print("Yep, I know", attr) tellme = "It's a secret" smarty = SmartyPants() smarty.name = "Smartinius Smart" smarty.quicksort # Yep, I know quicksort smarty.python # Yep, I know python smarty.tellme # "It's a secret" smarty.name # "Smartinius Smart"
__getattribute__(self, name) будет вызван при попытке получить значение атрибута. Если этот метод переопределён, стандартный механизм поиска значения атрибута не будет задействован. Следует иметь ввиду, что вызов специальных методов (например __len__(), __str__()) через встроенные функции или неявный вызов через синтаксис языка осуществляется в обход __getattribute__().
class Optimist: attr = "class attribute" def __getattribute__(self, name): print(" is great!".format(name)) def __len__(self): print("__len__ is special") return 0 o = Optimist() o.instance_attr = "instance" o.attr # attr is great! o.dark_beer # dark_beer is great! o.instance_attr # instance_attr is great! o.__len__ # __len__ is great! len(o) # __len__ is special\n 0
__setattr__(self, name, value) будет вызван при попытке установить значение атрибута экземпляра. Аналогично __getattribute__(), если этот метод переопределён, стандартный механизм установки значения не будет задействован:
class NoSetters: attr = "class attribute" def __setattr__(self, name, val): print("not setting =".format(name,val)) no_setters = NoSetters() no_setters.a = 1 # not setting a=1 no_setters.attr = 1 # not setting attr=1 no_setters.__dict__ # <> no_setters.attr # "class attribute" no_setters.a # AttributeError
__delattr__(self, name) — аналогичен __setattr__(), но используется при удалении атрибута.
При переопределении __getattribute__(), __setattr__() и __delattr__() следует иметь ввиду, что стандартный способ получения доступа к атрибутам можно вызвать через object:
class GentleGuy: def __getattribute__(self, name): if name.endswith("_please"): return object.__getattribute__(self, name.replace("_please", "")) raise AttributeError("And the magic word!?") gentle = GentleGuy() gentle.coffee = "some coffee" gentle.coffee # AttributeError gentle.coffee_please # "some coffee"
Соль
- Если определён метод a.__class__.__getattribute__(), то вызывается он и возвращается полученное значение.
- Если attrname это специальный (определённый python-ом) атрибут, такой как __class__ или __doc__, возвращается его значение.
- Проверяется a.__class__.__dict__ на наличие записи с attrname. Если она существует и значением является дескриптор данных, возвращается результат вызова метода __get__() дескриптора. Также проверяются все базовые классы.
- Если в a.__dict__ существует запись с именем attrname, возвращается значение этой записи. Если a — это класс, то атрибут ищется и среди его базовых классов и, если там или в __dict__a дескриптор данных — возвращается результат __get__() дескриптора.
- Проверяется a.__class__.__dict__, если в нём существует запись с attrname и это “дескриптор не данных”, возвращается результат __get__() дескриптора, если запись существует и там не дескриптор, возвращается значение записи. Также обыскиваются базовые классы.
- Если существует метод a.__class__.__getattr__(), он вызывается и возвращается его результат. Если такого метода нет — выкидывается AttributeError.
- Если существует метод a.__class__.__setattr__(), он вызывается.
- Проверяется a.__class__.__dict__, если в нём есть запись с attrname и это дескриптор данных — вызывается метод __set__() дескриптора. Также проверяются базовые классы.
- В a.__dict__ добавляется запись value с ключом attrname.
__slots__
Как пишет Guido в своей истории python о том, как изобретались new-style classes:
… Я боялся что изменения в системе классов плохо повлияют на производительность. В частности, чтобы дескрипторы данных работали корректно, все манипуляции атрибутами объекта начинались с проверки __dict__ класса на то, что этот атрибут является дескриптором данных…
На случай, если пользователи разочаруются ухудшением производительности, заботливые разработчики python придумали __slots__.
Наличие __slots__ ограничивает возможные имена атрибутов объекта теми, которые там указаны. Также, так как все имена атрибутов теперь заранее известны, снимает необходимость создавать __dict__ экземпляра.
class Slotter: __slots__ = ["a", "b"] s = Slotter() s.__dict__ # AttributeError s.c = 1 # AttributeError s.a = 1 s.a # 1 s.b = 1 s.b # 1 dir(s) # [ . 'a', 'b' . ]
Оказалось, что опасения Guido не оправдались, но к тому времени, как это стало ясно, было уже слишком поздно. К тому же, использование __slots__ действительно может увеличить производительность, особенно уменьшив количество используемой памяти при создании множества небольших объектов.
Заключение
Доступ к атрибутом в python можно контролировать огромным количеством способов. Каждый из них решает свою задачу, а вместе они подходят практически под любой мыслимый сценарий использования объекта. Эти механизмы — основа гибкости языка, наряду с множественным наследованием, метаклассами и прочими вкусностями. У меня ушло некоторое время на то, чтобы разобраться, понять и, главное, принять это множество вариантов работы атрибутов. На первый взгляд оно показалось слегка избыточным и не особенно логичным, но, учитывая, что в ежедневном программировании это редко пригодиться, приятно иметь в своём арсенале такие мощные инструменты.
Надеюсь, и вам эта статья прояснила парочку моментов, до которых руки не доходили разобраться. И теперь, с огнём в глазах и уверенностью в Точке, вы напишите огромное количество наичистейшего, читаемого и устойчивого к изменениям требований кода! Ну или комментарий.
Спасибо за ваше время.
Ссылки
- Shalabh Chaturvedi. Python Attributes and Methods
- Guido Van Rossum. The Inside Story on New-Style Classes
- Python documentation
