Cat

Питон на измене

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

Нюансы Python rusheslav 26 Ноябрь 2023 Просмотров: 428

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

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

 

Основные изменяемые и неизменяемые объекты

Как мы знаем, в переменную мы можем записывать объекты разных типов. Также мы уже скорее всего понимаем (или хотя бы слышали), что все они делятся на две категории:

  1. Неизменяемые. Например: int (целые числа), float (числа с плавающей точкой), string (строки), tuple (кортежи), bool (булево значение или логический тип), frozenset(неизменяемое множество).
     
  2. Изменяемые. Например: list (список), set (множество), dict (словарь).

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

 

Что же означает это свойство - изменяемость?

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

students = "ученики четвертого класса"
print(type(students), id(students))  # <class 'str'> 4476735024
students = ["Стэн", "Эрик", "Кайл", "Кенни"]
print(type(students), id(students))  # <class 'list'> 4477400320


Как мы видим, название переменной осталось тем же, но вместе с типом данных содержимого изменилась и ячейка памяти, которая его хранит.

А что происходит, когда тип содержимого остается прежним, но мы лишь слегка изменяем сам объект? Оказывается, что таким вот образом «влезть» можно только в те коробки, где хранятся изменяемые типы данных (например, списки). Когда же мы помещаем в коробку неизменяемый тип данных (например, строку), коробка как будто «запечатывается» и вскрыть её, чтобы внести изменения в содержимое, уже не получится.

Рассмотрим не примере:

students_list = ["Стэн", "Эрик", "Кайл"]
print(id(students_list), students_list)  # 4516377856 ['Стэн', 'Эрик', 'Кайл']
students_list.append("Кенни")
print(id(students_list), students_list)  # 4516377856 ['Стэн', 'Эрик', 'Кайл', 'Кенни']
students_str = "Стэн, Эрик, Кайл"
print(id(students_str), students_str)  # 4516970768 Стэн, Эрик, Кайл
students_str += ", Кенни"
print(id(students_str), students_str)  # 4515712560 Стэн, Эрик, Кайл, Кенни


Мы видим, что добавление ещё одного элемента в список не меняет его местонахождения. Это по-прежнему тот же список в той же самой ячейке памяти.

А вот когда мы совершаем те или иные манипуляции со строкой, это не изменяет её (то есть сам первоначальный объект), и это важный для понимания момент. Когда мы проводим конкатенацию (слияние) двух строк - “Стэн, Эрик, Кайл” и “, Кенни”, мы не меняем ни одну из них, а создаем новую строку в новой ячейке памяти, что нам и показывает функция id().

 

А что же происходит с первоначальной строкой?

В нашем примере она удаляется сборщиком мусора, чтобы освободить память. И в данном случае «сборщик мусора» (garbage collector) - это даже не метафора. Так действительно называется алгоритм очистки памяти Python, с которым мы обязательно познакомимся поближе как-нибудь в другой раз.

А вот если бы на первоначальную строку ссылалась ещё одна переменная, то в результате выполнения конкатенации мы увидели бы, что наша строка “Стэн, Эрик, Кайл” тихо-мирно лежит себе всё в той же самой коробочке, в которой и была:

students_str = "Стэн, Эрик, Кайл"
students_str_copy = students_str
print(id(students_str), students_str)  # 4372726032 Стэн, Эрик, Кайл
print(id(students_str_copy), students_str_copy)  # 4372726032 Стэн, Эрик, Кайл
students_str += ", Кенни"
print(id(students_str), students_str)  # 4371484208 Стэн, Эрик, Кайл, Кенни
print(id(students_str_copy), students_str_copy)  # 4372726032 Стэн, Эрик, Кайл


Вспомним пример из поста про оператор “==“ и ключевое слово “is”. Тогда мы нечто подобное проделывали со списком, на который ссылались две переменные. Когда мы изменяли список, используя первую переменную, то это изменение было видно и во второй переменной. Потому что обе они по-прежнему ссылались на всё тот же объект, который хранится всё в той же ячейке, что и первоначально. Почему? Потому что списки - изменяемый тип данных, и «коробка», в которой он лежит, не «запечатана».

 

А зачем это нужно? Почему все объекты не могут быть изменяемыми?

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

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

favorite_cartoon = "Гадкий я"
locker_image = favorite_cartoon
lockers = {
   favorite_cartoon: ["Куртка", "Шапка", "Водяной пистолет"],
   "Шрек": ["Шарф", "Жирафик"],
   "Гадкий я 2": ["Свитер", "Резиновый ёжик"]
}
favorite_cartoon += " 2"


В нашем примере у мальчика в детском саду есть любимый мультфильм - «Гадкий я». Он выбрал себе шкафчик с постером этого мультика. В коде это отображается так: переменная favorite_cartoon ссылается на строку “Гадкий я”. Далее к ней же прикрепляется другая переменная - locker_image, объект которой становится ключом в словаре шкафчиков детского сада.

Спустя какое-то время предпочтения мальчика меняются, и он становится фанатом второй части мультфильма. В коде это отображается конкатенацией строк «Гадкий я» и « 2», результат которой перезаписывается в переменную favorite_cartoon. Если бы строка была изменяемым типом, это привело бы к изменению объекта в той же самой ячейке памяти. А так как на неё же у нас ссылается и переменная locker_image, это привело бы к тому, что в словаре появилось бы две пары ключ-значение с одинаковым ключом, потому что в нашем детском саду уже был шкафчик с постером «Гадкий я 2».

 

А почему бы тогда не сделать все объекты неизменяемыми?

Изменяемость некоторых типов данных позволяет экономить значительные объёмы памяти за счёт возможности редактировать объект прямо в текущей ячейке, а не создавать новый. Представим, что у нас есть журнал с оценками школьников. Если бы мы записывали их в неизменяемый список (то есть по сути в кортеж), нам бы пришлось для записи каждой новой оценки создавать ещё один кортеж. Это как если бы каждый раз, выставляя оценку, учитель заполнял бы новый журнал. Со временем пришлось бы заводить отдельный архив для хранения накопившихся “одноразовых” журналов с оценками учеников одного-единственного класса.

Это далеко не всё, что можно было бы сказать об изменяемости объектов в Python, но всему своё время…

    Нет комментариев

    Реклама