Шаг 79.
Python: тонкости программирования. Классы и ООП. Переменные класса против переменных экземпляра: подводные камни (общие сведения)

    На этом шаге мы рассмотрим особенности использования переменных класса и объекта.

    Помимо проведения различия между методами класса и методами экземпляра, объектная модель Python также проводит различие между переменными класса и переменными экземпляра.

    Это различие имеет большое значение. В этом и последующих шагах мы устраним путаницу относительно этой темы при помощи нескольких практических примеров.

    В объектах Python объявляются два вида атрибутов данных:

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

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

    Ладно, все это было довольно абстрактно - самое время рассмотреть немного исходного кода! Давайте потренируемся на собачках... В обучающих пособиях, посвященных ООП, для иллюстрации этого тезиса всегда используются автомобили или домашние животные, и мы не будем отказываться от этой традиции.

    Что собаке для счастья нужно? Правильно! Четыре лапы да имя:

>>> class Dog:
	num_legs = 4 # <- Переменная класса
	def __init__(self, name):
		self.name = name # <- Переменная экземпляра

    О'кей. У нас есть изящное объектно-ориентированное представление ситуации с собакой, которую мы только что описали. Создание новых экземпляров Dog работает, как и ожидалось, и каждый из них получает переменную экземпляра с именем name:

>>> jack = Dog('Джек')
>>> jill = Dog('Джилл')
>>> jack.name, jill.name
('Джек', 'Джилл')

    Во всем, что касается переменных класса, всегда есть чуть больше гибкости. Доступ к переменной класса num_legs можно получить либо непосредственно в каждом экземпляре Dog, либо в самом классе:

>>> jack.num_legs, jill.num_legs
(4, 4)
>>> Dog.num_legs
4

    Однако попытка получить доступ к переменной экземпляра через класс потерпит неудачу с исключением AttributeError. Переменные экземпляра характерны для каждого экземпляра объекта и создаются, когда выполняется конструктор __init__ - они даже не существуют в самом классе.

    В этом заключается ключевое различие между переменными класса и переменными экземпляра:

>>> Dog.name
Traceback (most recent call last):
.   .   .   .
AttributeError: type object 'Dog' has no attribute 'name'

    Ладно, пока все идет неплохо.

    Допустим, в один прекрасный день пес по кличке Джек поедал свой ужин, расположившись слишком близко от микроволновки, в результате у него выросла дополнительная пара лап. Как бы вы представили этот факт в небольшой "песочнице" с исходным кодом, которая у нас сейчас есть?

    Первая идея - просто модифицировать переменную num_legs в классе Dog:

>>> Dog.num_legs = 6

    Но помните, мы не хотим, чтобы все собаки стали носиться вокруг о шести лапах. Итак, сейчас мы только что превратили каждую собаку в нашей микровселенной в сверхсобаку, потому что мы модифицировали переменную класса. Это затрагивает всех собак, даже тех, которые были созданы ранее:

>>> jack.num_legs, jill.num_legs
(6, 6)

    Этот вариант не сработал. А не сработал он потому, что модификация переменной класса в пространстве имен класса затрагивает все экземпляры класса. Давайте отыграем это изменение в переменной класса назад и вместо этого попробуем дать дополнительную пару лап только конкретному псу Джеку:

>>> Dog.num_legs = 4 
>>> jack.num_legs = 6

    Так, и что за чудовище мы получили? Сейчас выясним:

>>> jack.num_legs, jill.num_legs, Dog.num_legs
(6, 4, 4)

    Ладно. Выглядит "довольно неплохо" (ну, кроме того, конечно, что мы прямо сейчас дали бедному псу несколько лишних лап). Но как это изменение на самом деле повлияло на наши объекты Dog?

    А проблема, как выясняется, здесь в следующем: несмотря на то что мы получили желаемый результат (лишние лапы для Джека), мы внесли переменную экземпляра num_legs в экземпляр с псом по кличке Джек. И теперь новая переменная экземпляра num_legs "оттеняет" переменную класса с тем же самым именем, переопределяя и скрывая ее, когда мы обращаемся к области действия экземпляра:

>>> jack.num_legs, jack.__class__.num_legs
(6, 4)

    Как вы видите, переменные класса, казалось бы, стали несогласованными. Это произошло потому, что внесение изменения в jack.num_legs создало переменную экземпляра с тем же самым именем, что и у переменной класса.

    Это не всегда плохо, но важно понимать, что именно здесь произошло.

    Сказать по правде, попытка модифицировать переменную класса через экземпляр объекта, который затем непредумышленно создает переменную экземпляра с тем же именем, затеняя оригинальную переменную класса, является в Python чем-то вроде подводного камня ООП.

    На следующем шаге мы продолжим изучение этого вопроса.




Предыдущий шаг Содержание Следующий шаг