Детективная история про указатели и память
Эта статья для тех, кто пишет в Python и сталкивается с некоторыми странностями. Если вы не пробовали Python, то почему? Это ж интересно. Вот несколько симпатичных статей в тему:
Теперь к нашим странностям. Вы наполняете список в Python какими-то значениями, используя элегантные шаблоны. При просмотре оказывается, что значения другие. Причём если наполнять список тупым методом копипасты, всё работает, а если умным методом с шаблонами — всё ломается.
Возможная причина — в список добавились не сами элементы, а ссылки на них.
Матчасть: указатели
Чтобы лучше понять, почему так бывает, нам понадобится выжимка из статьи про указатели:
- когда мы заводим новую переменную, компьютер выделяет для неё место в оперативной памяти;
- после этого компьютер запоминает адрес ячейки, где хранится переменная, и когда нужно что-то с ней сделать — обращается по этому адресу;
- указатель — это переменная, где хранится адрес ячейки памяти; он указывает на какую-то переменную, но обращается к ней не по имени, а по адресу.
Ситуация: Python меняет значения списка
Представим, что мы пишем модуль, который добавляет новых пользователей. Чтобы данные о каждом пользователе были в одном стиле, мы заводим словарь с уже готовыми полями, который потом используем как шаблон.
Логика такая: когда нужно добавить нового пользователя, мы копируем содержимое этого шаблона в другую переменную, заполняем все данные, а потом добавляем её в массив пользователей:
В консоли видно, что всё работает как нужно: пользователь добавлен в массив, а значит, можно продолжать писать код. Добавим таким же способом второго пользователя:
Странно, но вместо того, чтобы добавить второго пользователя, мы получили в массиве две одинаковые записи. Как это может быть, если мы просто добавили новый элемент командой append () и ничего больше с массивом не делали?
Причина: в массив добавляются не элементы, а ссылки на них
Ссылки — это те же самые указатели, только привязанные к конкретной переменной. В нашем случае логика компьютера такая:
- Когда мы объявляем словарь user_info = {'Name':'','Work':''}, то компьютер создаёт новую переменную, выделяет под неё память и кладёт туда стартовое значение.
- Когда мы переменной new_user присваиваем значение user_info, то компьютер, чтобы не тратить память зря, кладёт в new_user не значение другой переменной, а ссылку на неё.
- Дальше мы начинаем заполнять переменную new_user, и компьютер понимает, что ссылка тут не подходит, нужно работать со значением. В этот момент он всё-таки выделяет для неё новую область памяти и кладёт туда значение из user_info — словарь {'Name':'','Work':''}.
- А когда мы добавляем эту переменную как элемент списка, то компьютер снова для экономии памяти отправляет в список не значение, а ссылку на переменную new_user. В итоге в первом элементе списка лежит не переменная, а ссылка.
- При выводе списка на экран компьютер по ссылке находит переменную new_user, берёт её значение и подставляет в массив — так мы видим на экране как бы правильную запись.
Когда мы добавляем второго пользователя, то компьютер делает всё точно так же: меняет значение переменной new_user и добавляет в массив ссылку на неё, тоже для экономии памяти.
В итоге в users вместо конкретных значений лежат две ссылки на одну и ту же переменную, в которой хранится только последнее значение. Со стороны кажется, что компьютер сам всё поменял, но на самом деле он сделал ровно то, что от него просили (как ему кажется).
Что делать
Ссылки в Python подставляются по умолчанию вместо сложных переменных: списков, кортежей, массивов или словарей. Чтобы исправить нашу ситуацию и работать в Python со значениями, а не со ссылками, есть два способа:
- Создавать нового пользователя не из шаблона, а напрямую, подставив значение словаря: new_user = {'Name':'','Work':''}. В этом случае компьютер сразу выделит память для значения и будет работать с ним.
- Подключить модуль copy и использовать команду deepcopy — она принудительно скопирует в новую переменную не ссылку, а значение: new_user = copy.deepcopy(user_info).