- Экземпляры
- Атрибуты класса и экземпляры
- Атрибут __dict__
- Проверка принадлежности экземпляра к классу
Экземпляры
Класс, как мы уже увидели, может хранить данные. Но типичный класс присутствует в программе в единственном экземпляре. Поэтому сам по себе класс не очень полезен, ведь хранить определения можно и в модулях. Весь смысл использования классов заключается в их инстанцировании.
Инстанцированием (instantiation) называют процесс (акт) создания на основе класса экземпляра (instance) — такого объекта, который получает доступ ко всему содержимому класса, но при этом обладает и способностью хранить собственные данные. При этом, имея объект, всегда можно узнать, экземпляром какого класса он является.
Давайте объявим класс и создадим пару экземпляров, а заодно и познакомимся с синтаксисом инстанцирования классов:
class Person:
pass
bob = Person()
bob # <__main__.Person object at 0x7f133df55fd0>
alice = Person()
alice # <__main__.Person object at 0x7f133df62048>
bob is alice # False
bob is Person # False
alice is Person # False
Что мы можем увидеть в этом примере? Первое, что бросается в глаза, это вызов класса как функции: Person()
. Сходство это — не только внешнее. В Python инстанцирование фактически и является вызовом некоторой функции, которая возвращает новый экземпляр класса.
При выводе объекта класса в REPL можно увидеть строку, похожую на вывод информации о классе, только вместо "class" в строчке упоминается "object".
Также стоит обратить внимание на то, что все экземпляры являются отдельными объектами, поэтому оператор is
дает False
как при соотнесении экземпляров между собой, так и при соотнесении любого экземпляра с объектом класса (bob
, alice
и Person
— три самостоятельных объекта).
Атрибуты класса и экземпляры
В предыдущем примере класс был пустой. Теперь воспроизведем его, но добавим на этот раз атрибут:
class Person:
name = 'Noname'
bob, alice = Person(), Person()
bob is alice # False
bob.name is alice.name # True
bob.name is Person.name # True
bob.name # 'Noname'
Этот пример показывает, что а) и bob
, и alice
имеют атрибут name
, б) значение атрибутов name
— общее для всех трех объектов.
Давайте же переименуем Боба:
bob.name = 'Bob'
bob.name is Person.name # False
Person.name # 'Noname'
alice.name # 'Noname'
bob.name # 'Bob'
Вот вы и увидели то самое "собственное состояние объекта". Person
продолжает давать имя всем экземплярам, пока те не изменят значение своего атрибута. В момент присваивания нового значения атрибуту экземпляра, экземпляр получает свой собственный атрибут!
Атрибут __dict__
Стоит прямо сейчас заглянуть "под капот" объектной системы Python, чтобы вы в дальнейшем могли исследовать объекты самостоятельно. Это и интересно, и полезно — как при обучении, так и при отладке объектного кода.
Итак, внутри каждого объекта Python хранит… словарь! Имена атрибутов в пространствах имен выступают ключами этого словаря, а значения являются ссылками на другие объекты. Словарь этот всегда называется __dict__
и тоже является атрибутом. Обращаясь к этому словарю, вы можете получить доступ к значениям атрибутов:
Person.__dict__['name'] # 'Noname'
bob.__dict__['name'] # 'Bob'
alice.__dict__['name']
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 'name'
Присмотритесь, и вы увидите: у bob
в __dict__
есть его собственное имя, а у alice
собственного имени нет. Но при обращении к атрибуту привычным способом "через точку", вы видите имя и у alice
! Как же это работает?
Дело в том, что машинерия объектной системы Python при обращении к атрибуту сначала ищет атрибут в словаре экземпляра. Но если там соответствующего ключа не нашлось, то атрибут ищется уже в классе. Именно так alice
получает имя: Python находит его в классе Person
.
Надо сказать, что это очень разумный подход! Да, Python мог бы копировать словарь класса при инстанцировании. Но это привело бы к излишнему потреблению памяти. А вот "коллективное использование", напротив, позволяет память экономить!
И, конечно же, словарь __dict__
объекта может быть изменен. Когда мы давали Бобу имя, мы на самом деле сделали что-то такое:
bob.__dict__['name'] = 'Bob'
Мы даже можем добавить Бобу фамилию и сделать это через модификацию __dict__
:
bob.__dict__['surname'] = 'Smith'
bob.surname # 'Smith'
'surname' in Person.__dict__ # False
А ведь у класса не было атрибута surname
! Каждый экземпляр класса тоже является самостоятельным пространством имен, пригодным для расширения в процессе исполнения программы (за счет использования под капотом словарей, как вы теперь знаете!).
Проверка принадлежности экземпляра к классу
Выше мы уже упоминали, что объект всегда связан с классом. Эта связь заключается в наличии у экземпляра атрибута __class__
, который является ссылкой на объект класса:
bob.__class__ # <class '__main__.Person'>
bob.__class__ is Person # True
Как вы уже могли заметить, в Python многие "внутренние штуки" имеют имена, заключенные в двойные символы подчеркивания. В разговоре питонисты обычно проговаривают подобные имена примерно так: "дАндер-класс", что является калькой с "dunder class", где "dunder", в свою очередь, это сокращение от "double underscore", то есть "двойной символ подчеркивания". Полезно запомнить этот стиль именования!
А еще стоит запомнить, что практически всегда, когда вы хотите использовать что-то, названное в dunder-стиле, "есть способ лучше"! Так с __dict__
напрямую работать не приходится, потому что есть возможность обращаться к атрибутам "через точку". Вот и __class__
в коде встречается редко. А рекомендуемый способ проверки принадлежности к классу выглядит так:
isinstance(bob, Person) # True
Запомните эту функцию! Она "умнее", чем вам может сейчас показаться. В дальнейшем мы увидим более интересные примеры ее применения.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.