Разбираемся в разнице
Помимо проведения различия между методами класса и методами экземпляра, объектная модель Python также приводит различие между переменными класса и переменными экземпляра.
Это различие имеет большое значение. Мне, как начинающему разработчику на Python, оно также доставляло немало хлопот. В течение длительного времени я не могу найти время, чтобы разобраться в этих понятиях с самых азов.
Поэтому мои первые эксперименты с ООП были пронизаны удивительными линиями поведения и странными ошибками. В этой статье мы устраним путаницу относительно этой темы при помощи нескольких практических примеров.
Как я писал ранее, в объектах Python объявляются два вида атрибутов данных: переменные класса (class variables) и переменные экземпляра (instance variables):
Переменные класса
- Объявляются внутри определения класса (но за пределами любых методов экземпляра). Они не привязаны ни к одному конкретному экземпляру класса.
- Вместо этого переменные класса хранят свое содержимое в самом классе, и все объекты, созданные на основе того или иного класса, предоставляют общий доступ к одинаковому набору переменных класса. Например, это означает, что модификация переменной класса одновременно затрагивает все экземпляры объекта.
Переменные экземпляра
- Всегда привязаны к конкретному экземпляру объекта. Их содержимое хранится не в классе, а в каждом отдельном объекте, созданном на основе класса.
- По этой причине содержимое переменной экземпляра абсолютно независимо от одного экземпляра объекта к другому. И поэтому модификация переменной экземпляра одновременно затрагивает только один экземпляр объекта.
Примеры кода
Ладно, это все было довольно абстрактно — самое время рассмотреть немного исходного кода! Давайте потренируемся на собачках... В обучающих пособиях, посвященных ООП, для иллюстрации этого тезиса всегда используются автомобили или домашние животные, и мне сложно отказаться от этой традиции.
Что собаке для счастья нужно? Правильно! Четыре лапы да имя (ну может еще хвост):
Oкей. У нас есть изящное объектно-ориентированное представление ситуации с собакой, которую я только что описал. Создание новых экземпляров Dog работает, как и ожидалось, и каждый из них получает переменную экземпляра с именем name:
Во всем, что касается переменных класса всегда есть чуть больше гибкости. Доступ к переменной класса num_legs можно получить либо непосредственно в каждом экземпляре Dog, либо в самом классе:
Однако попытка получить доступ к переменной экземпляра через класс потерпит неудачу с исключением AttributeError. Переменные экземпляра характерны для каждого экземпляра объекта и создаются, когда выполняется конструктор __init__ — они даже не существуют в самом классе.
В этом и заключается ключевое различие между переменными класса и переменными экземпляра:
Ладно, пока все идет неплохо.
Изменение атрибутов
Допустим, в один прекрасный день пес по кличке Джек поедал свой ужин, расположившись слишком близко от микроволновки, в результате у него выросла дополнительная пара лап.
Как бы вы представили этот факт в небольшой "песочнице" с исходным кодом, которая у нас сейчас есть?
Первая идея — просто модифицировать переменную num_legs в классе Dog:
>>> Dog.num_legs = 6
Но помните, мы не хотим, чтобы все собаки стали носиться вокруг о шести лапах. Итак, сейчас мы только превратили каждую собаку в нашей микровселенной в сверхсобаку, потому что мы модифицировали переменную класса.
Это затрагивает всех собак, даже тех, которые были созданы ранее:
Этот вариант не сработал. А не сработал он потому, что модификация переменной класса в пространстве имен класса затрагивает все экземпляры класса.
Давайте отыграем это изменение в переменной класса назад и вместо этого попробуем дать дополнительную пару лап только конкретному псу Джеку:
Так, и что же за чудовище мы получили? Сейчас выясним:
Ладно, выглядит "довольно неплохо" (ну, кроме того, конечно, что мы прямо сейчас дали бедному псу несколько лишних лап). Но как это изменение на самом деле повлияло на наши объекты Dog?
А проблема, как выясняется здесь в следующем: несмотря на то, что мы получили желаемый результат (лишние лапы для Джека), мы внесли переменную экземпляра num_legs в экземпляр с псом по кличке Джек. И теперь новая переменная экземпляра num_legs "оттеняет" переменную класса с тем же самым именем, переопределяя и скрывая ее, когда мы обращаемся к области действия экземпляра:
Как вы видите, переменные класса, казалось бы, стали несогласованными. Это произошло потому, что внесения изменения в jack.num_legs создало переменную экземпляра с тем же самым именем, что и у переменной класса.
Это не всегда плохо, но важно понимать, что именно здесь произошло. Прежде чем я наконец-то разобрался в области действия уровня класса и уровня экземпляра в Python, это было довольно широкими воротами, через которые в мои программы то и дело закрадывались ошибки.
Сказать по правде, попытка модифицировать переменную класса через экземпляр объекта, который затем непредумышленно создает переменную экземпляра с тем же именем, затеняя оригинальную переменную класса, является в Python чем-то вроде подводного камня ООП.
Понравилась статья? Не забудь подписаться и оставить свое мнение в комментариях, обязательно прочту и отвечу.
Читайте также: