Изменяемые vs. неизменяемые типы данных в Python

Python считается одним из самых удивительных языков программирования. Многие люди выбирают его в качестве первого языка из-за его элегантности и простоты. Благодаря широкому сообществу, избытку пакетов и согласованности синтаксиса, опытные профессионалы также используют Python. Тем не менее, существует одна вещь, которая раздражает как новичков, так и некоторых профессиональных разработчиков – объекты Python.

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

Как известно, объектом в Python является абсолютно все, а каждый объект относится к какому-либо типу данных. Типы данных бывают изменяемые и неизменяемые (англ. mutable и immutable). К неизменяемым относятся целые числа (int), числа с плавающей запятой (float), булевы значения (bool), строки (str), кортежи (tuple). К изменяемым — списки (list), множества (set), байтовые массивы (byte arrays) и словари (dict).

Функции id() и type()

Разобраться с изменяемостью типов данных нам помогут встроенные функции и операторы Python.

Встроенный метод id() возвращает идентификатор объекта в виде целого числа. Это целое число обычно относится к месту хранения объекта в памяти. Встроенная функция type() возвращает тип объекта.

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

Рассмотрим пример.

Если сравнить две переменные, x и y, имеющие одинаковое значение, при помощи оператора равенства (x == y), он выдаст True. Но если мы при помощи того же оператора сравним идентификаторы объектов x и y (полученные с использованием функции id()) – мы получим False. Дело в том, что во втором случае мы сравнивали адреса памяти переменных, а они разные – расположены в разных местах. Хотя значения, которые содержат эти переменные, одинаковы.

x = 'Привет!'
y = 'Привет!'
x == y   # Получим True
id(x) == id(y)   # Получим False

Создадим переменную z путем присвоения ей в качестве значения переменной x. Используя оператор is, мы обнаружим, что обе переменные указывают на один и тот же объект и, соответственно, имеют одинаковые идентификаторы.

x = 'Привет!'
z = x
x is z   # Получим True
id(x) == id(z)   # Получим True

Неизменяемые типы данных

Давайте рассмотрим некоторые неизменяемые типы.

Целые числа (int)

Давайте определим переменную x, имеющую значение 10. Встроенный метод id() используется для определения местоположения x в памяти, а type() используется для определения типа переменной. Когда мы пытаемся изменить значение x, оно успешно изменяется.

Стоит заметить, что адрес памяти тоже изменяется. Так происходит потому, что фактически мы не изменили значение x, а создали другой объект с тем же именем x и присвоили ему другое значение. Мы связали имя x с новым значением. Теперь, когда вы вызываете x, он будет выводить новое значение и ссылаться на новое местоположение.

x = 10
print(x, type(x), id(x))
# Получим (10, int, 140727505991744)
x = 12
print(x, type(x), id(x))
# Получим (12, int, 140727505991808)

Строки (str)

То же самое верно и для строкового типа данных. Мы не можем изменить существующую переменную, вместо этого мы должны создать новую с тем же именем.

В данном примере мы определили строковую переменную x, но допустили ошибку в слове и теперь хотим исправить «ю» на «и». Однако мы получаем TypeError. Это показывает, что строковые объекты не подлежат обновлению.

x = 'Прювет!'
x[2] = 'и'
# Получим TypeError: 'str' object does not support item assignment

Кортежи (tuple)

Давайте разберем кортежи. Мы определили кортеж с 4 значениями. Воспользуемся функцией id() для вывода его адреса. Если мы захотим изменить значение первого элемента, то получим ошибку TypeError. Это означает, что кортеж не поддерживает присвоение или обновление элементов.

tuple1 = (1, 2, 3, 4)
print(tuple1, id(tuple1))
# Получим (1, 2, 3, 4) 3240603720336

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

tuple1 = (5, 6, 7, 8)
print(tuple1, id(tuple1))
# Получим (5, 6, 7, 8) 3240603720256

Числа с плавающей запятой (float)

У нас есть переменная x типа float. Используя функцию id(), мы можем узнать ее адрес. Если мы попробуем заменить элемент с индексом 1, то получим TypeError. Как и в предыдущих примерах видим, что float не поддерживает модификацию элемента.

x = 3.456
x[1] = 4
# Получим TypeError: 'float' object does not support item assignment

Если же мы обновим float, переопределив его, то при вызове получим новое значение и новый адрес.

x = 3.654
type(x), id(x)
# Получим (float, 3240603166960)

Изменяемые типы данных

Теперь давайте рассмотрим некоторые изменяемые типы.

Списки (list)

Определим список с именем x и добавим в него некоторые значения. После этого обновим список: присвоим новое значение элементу с индексом 1. Можем заметить, что операция успешно выполнилась.

x = ['Яблоко', 'Груша', 'Слива']
x[1] = 'Ананас'
x   # выведет ['Яблоко', 'Ананас', 'Слива'] 

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

Создадим новое имя y и свяжем его с тем же объектом списка. А теперь проверим, совпадает ли x с y. Нам вернется True. Кроме того, x и y имеют одинаковые адреса памяти.

x = y =  ['Яблоко', 'Груша', 'Слива']
x is y   # True
id(x), id(y), id(x) == id(y)   # (3240602862208, 3240602862208, True)

Теперь добавим новое значение к списку x и проверим обновленный вывод.

x.append('Персик')
x   # ['Яблоко', 'Груша', 'Слива', 'Персик']

Если мы теперь вызовем y, то получим тот же список, что и при вызове x. Хотя непосредственно в y мы ничего не добавляли. Это означает, что по сути мы обновляем один список объектов, у которого есть два разных имени: x и y. Оба они одинаковы и имеют один адрес в памяти даже после модификации.

y   # ['Яблоко', 'Груша', 'Слива', 'Персик']
x is y, id(x) == id(y)   # (True, True)

Словари (dict)

Словари — часто используемый тип данных в Python. Давайте посмотрим на их изменчивость.

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

dict = {'Name':'Алиса', 'Age':27, 'Job':'Senior Python Developer'}
dict
# Получим {'Name': 'Алиса', 'Age': 27, 'Job': 'Senior Python Developer'}

dict['Name'], dict['Age'], dict['Job']
# ('Алиса', 27, 'Senior Python Developer')

Давайте изменим какое-нибудь значение в нашем словаре. Например, обновим значение для ключа Name. Выведем обновленный словарь. Значение изменилось. При этом сами ключи словаря неизменяемы.

dict['Name'] = 'Роберт'
dict   # {'Name': 'Роберт', 'Age': 27, 'Job': 'Senior Python Developer'}

Списки и кортежи: наглядный пример изменяемых и неизменяемых объектов

Давайте по отдельности определим список и кортеж. Убедимся, что в кортеже есть значение типа список, а в списке есть значение типа кортеж.

tuple1 = ([1, 1], 2, 3)
list1 = [(1, 1), 2, 3]
tuple1, list1
# (([1, 1], 2, 3), [(1, 1), 2, 3])

Нулевой элемент кортежа – список. Давайте попробуем изменить какой-то из элементов списка, указав его индекс. Например, можно поменять в нулевом элементе кортежа (т.е. в списке) нулевой элемент. Нам успешно удается это сделать, потому что список – изменяемый объект, даже если он находится в кортеже.

tuple1[0][0] = 'Поменялся'
tuple1
# (['Поменялся', 1], 2, 3)

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

list1[0][0] = 'Поменялся'
list1
# Получим TypeError: 'tuple' object does not support item assignment

Заключение

Мы разобрали различия между изменяемым и неизменяемым объектами в Python. Стоит понимать, что всё в Python называется объектами. И главное различие между ними – являются они изменяемыми или неизменяемыми.