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