Python 3: изменяемый, неизменяемый…

Многие из нас считают Python прекрасным языком программирования. В нем легко разобраться, он очень хорошо читается, а код на нем легко поддерживать. Но основная причина простоты этого языка состоит в том, что под капотом у него есть очень много всего. И хотя многие люди просто знают, что в языке есть и сложные вещи, и им этого достаточно, понимание тонкостей поможет вам избежать появления многих багов в ваших программах. Цель этой статьи — познакомить вас с этими тонкостями.

Объекты

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

Чтобы понять эту концепцию, мы можем представить, что объекты — это отдельные люди, а классы — это группы, к которым эти люди принадлежат. Скажем, все мы — люди (класс human), но я — уникальный индивид (экземпляр класса human).

В Python все является объектом. Каждый объект имеет собственный тип данных и внутреннее состояние (данные). Давайте для начала уясним, как данные хранятся в памяти. Для этого рассмотрим пару примеров.

Пример 1

Когда Python выполняет a = 1000, это создает числовой объект где-то в памяти (допустим, по адресу 0x1111), где сохраняется значение 1000. При этом a не хранит значение 1000, она хранит ссылку на этот объект (адрес в памяти). Когда выполняется следующее предложение a = 1500, в памяти (допустим, по адресу 0x2222) создается другой объект со значением 1500, и теперь a хранит ссылку на этот новый объект.

a = 1000
a = 1500

Пример 2

Когда Python выполняет a = 2000, как объяснялось в предыдущем примере, создается числовой объект в памяти по адресу 0x3333, и переменная a указывает на этот объект. После выполнения b = a переменная b тоже начинает указывать на тот же адрес в памяти, что и a. Обратите внимание, что не значение a копируется в b, а ссылка (адрес в памяти).

a = 2000
b = a

Идентичность и тип

Прежде чем мы погрузимся в пучину объектов в Python, давайте рассмотрим, что стоит за двумя встроенными функциями: id() и type(). Эти две функции способны предоставить нам критически важную информацию о переменной.

Функция type() вернет тип данных объекта, который чаще всего совпадает с классом, к которому принадлежит этот объект. Например, если у меня есть список l, выполнение type(l) вернет класс list:

Функция id() возвращает идентификатор указанной вами переменной. Каждая переменная в Python ссылается на какой-нибудь объект, а идентификатор переменной это целое число, «привязанное» к конкретному объекту. В реализации CPython это число — адрес объекта в памяти. Идентификатор объекта позволяет разграничить случаи, когда переменные идентичны, и когда они ссылаются на один и тот же объект. Для определения идентичности мы можем использовать «==», а «is» используется для определения того, указывают ли переменные на один и тот же объект.

Скажем, у нас есть два списка, x и y. Мы можем видеть, что у них разные идентификаторы:

Отсюда:

Но все станет куда более странно, если мы сделаем вот так:

Вероятно, вы думаете: «Погодите, погодите! Что?! Ведь пару секунд назад, когда мы рассматривали списки, я понял, что они разные. А эти тогда почему одинаковые?» Чтобы ответить на этот вопрос, мы должны углубиться в предмет изменяемости.

Изменяемые объекты

Итак, что же такое изменяемость (мутабельность)? Выражаясь простым языком, это способность мутировать — или изменять объект. К изменяемым (мутабельным) типам в Python относятся списки, множества, словари и bytearrays (массивы байтов). Давайте рассмотрим пример с нашим старым добрым списком. Мы можем добавлять в него элементы:

Мы можем удалять из него элементы:

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

Почему идентификатор l[1] изменился? Идентификатор всего списка остался неизменным, хотя мы изменили его значения, но почему же изменился идентификатор его элемента? Потому что числа и строки неизменяемы (не мутабельны).

Неизменяемые объекты

Неизменяемость это противоположность изменяемости (логично). Если объект неизменяемый, это значит, что вы не можете менять его содержимое. К встроенным неизменяемым типам относятся целые числа, числа с плавающей точкой, комплексные числа, строки, кортежи, frozenset-ы и байты.

В приведенном выше примере объект список изначально содержал ссылки на объекты «1», «2» и «3». Хотя мы не можем изменить сам объект (цифру 2), можно изменить список за счет изменения хранящейся в нем ссылки. Теперь список содержит ссылку на объект «Hello». Другими словами, произошло переназначение. И хотя увеличивать неизменяемый объект нельзя, мы можем сделать так:

Кажется, что мы добавили «World» к «Hello», но на самом деле произошло примерно следующее:

s = s + “ World”

Было вычислено значение s («Hello») плюс “ World”, и переменная s получила ссылку на новый объект. Похожий процесс происходит при добавлении чисел:

Но если вы попытаетесь изменить элемент в неизменяемом наборе, интерпретатор возбудит исключение:

Аналогично — хотя удалить элемент в неизменяемом объекте нельзя, вы можете взять срез объекта, в результате чего вернется новый объект, содержащий нужные значения:

Поскольку Python должен создавать отдельный объект для каждого уникального неизменяемого значения (что занимает много памяти), интерпретатор разумным образом оптимизирует создание объектов. Эта оптимизация происходит за счет использования одинаковых ссылок для неизменяемых объектов, таких как строки:

В CPython все еще круче: для облегчения доступа все общие маленькие int-объекты (от -5 (включительно) до 257) сохраняются в массиве. Другие объекты создаются при необходимости и удаляются, когда не остается переменных, ссылающихся на них, а эти маленькие int-объекты сохраняются в течение всей работы программы.

Преаллокация в Python

А теперь домашнее задание для вас:

  1. Создайте две переменные со значениями между -5 и 256, а затем проверьте, указывают ли они на один и тот же объект.
  2. Сделайте то же самое, но используйте значения вне указанного диапазона.

Что произошло?

При запуске Python3 сохраняет массив числовых объектов от -5 до 256 (включительно). Например, для объекта int используются макросы NSMALLPOSINTS и NSMALLNEGINTS. Давайте посмотрим исходный код:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif
#ifdef COUNT_ALLOCS
Py_ssize_t quick_int_allocs;
Py_ssize_t quick_neg_int_allocs;
#endif

Что это означает? Это значит, что когда вы создаете int из диапазона от -5 до 256, вы на самом деле ссылаетесь на уже существующий объект. Это сделано для того, чтобы избежать многократного создания часто используемых объектов, а кроме того таким образом вы можете представить любой символ ASCII.

Почему это важно?

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

И вот этот:

Почему y — тот же объект, что x? Ведь в первом примере они были разными объектами! В Python большое значение имеет то, как происходит присваивание. В этом случае мы не назначили переменной y другой объект. Вместо этого мы создали псевдоним для x. То есть сделали так, что переменная y получила ту же ссылку на объект, которая была у переменной x. Поскольку обе переменные теперь указывают на один объект, изменение одной переменной повлечет за собой изменение другой.

А что, если мы хотим создать копию объекта, чтобы при внесении изменений не волноваться о том, что это затронет оригинал? Мы можем взять срез списка, при этом вернется новый объект (так же, как когда мы брали срез неизменяемого объекта):

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

Правая сторона выражения была вычислена, результат был определен как [1, 2, 3, 4, 5], а затем был создан новый объект, ссылку на который получила переменная l. Но если мы сделаем так:

Мы видим, что объект остался прежним. Это замещающее присваивание — эквивалент добавления элементов в список. Но поскольку иммутабельные объекты не могут быть изменены, выражения a += b и a = a + b будут работать одинаково и аналогично нашему примеру «Hello World».

Передача аргументов в функции

В других языках программирования переменные часто передаются одним из двух способов:

  1. по значению (новая переменная получает то же значение, что и переданный аргумент)
  2. по ссылке (переменная хранит ссылку на данные, при этом данные могут быть изменены).

Но Python в этом плане уникален. Python передает ссылку на объект. Аналогично тому как мы делали переменную y псевдонимом переменной x (x = y), при передаче переменной в функцию аргумент функции становится псевдонимом для ссылки на объект.

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

Но поскольку списки мутабельны, их содержимое изменять можно:

Обратите внимание, что ссылка на список остается той же на протяжении всей программы. Тем не менее, если бы мы переприсвоили значение l внутри функции, это не затронуло бы исходный список.

Исключения в неизменяемости

Не все неизменяемые объекты на самом деле неизменяемы. Да, все очень запутанно, но сейчас разберемся.

Как уже говорилось, в Python контейнеры типа кортежей неизменяемы. Это значит, что значение tuple не может меняться после того, как кортеж создан. Но «значение» кортежа на самом деле является последовательностью имен с неизменяемыми привязками к объектам. Главное, что нужно понять, это что неизменяемы именно привязки, а не сами объекты.

Давайте посмотрим на кортеж t = (‘holberton’, [1, 2, 3]).

Кортеж t содержит элементы с разными типами данных. Первый элемент — неизменяемая строка, а второй — изменяемый список. Сам кортеж неизменяемый (нет никаких методов для изменения его содержимого). Аналогично и строка неизменяема, потому что у строк нет никаких методов для их изменения. Но объект список имеет методы, с помощью которых его можно изменить, так что он изменяемый. Это мелочь, но важная: «значение» неизменяемого объекта не может меняться, но объекты, из которых он состоит, — могут.

Заключение

Понимание тонкостей языка может быть чрезвычайно полезным. Многие программисты не уделяют достаточно времени тому, чтобы хорошенько изучить языки, которыми пользуются на постоянной основе. Но близкое знакомство с Python — отличный способ избежать багов.

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

Надеемся, эта статья была вам полезной!