Инстанциирование классов и экземпляры

Экземпляры.

Класс, как мы уже увидели, может хранить данные. Но типичный класс присутствует в программе в единственном экземпляре. Поэтому сам по себе класс не очень полезен, ведь хранить определения можно и в модулях. Весь смысл использования классов заключается в их инстанциировании.

Инстанциированием (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

Запомните эту функцию! Она "умнее", чем вам может сейчас показаться. В дальнейшем мы увидим более интересные примеры её применения.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →