Найти тему
Журнал «Код»

Что делать, когда Python сам меняет значения списка

Детективная история про указатели и память

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

Теперь к нашим странностям. Вы наполняете список в Python какими-то значениями, используя элегантные шаблоны. При просмотре оказывается, что значения другие. Причём если наполнять список тупым методом копипасты, всё работает, а если умным методом с шаблонами — всё ломается.

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

Матчасть: указатели

Чтобы лучше понять, почему так бывает, нам понадобится выжимка из статьи про указатели:

  • когда мы заводим новую переменную, компьютер выделяет для неё место в оперативной памяти;
  • после этого компьютер запоминает адрес ячейки, где хранится переменная, и когда нужно что-то с ней сделать — обращается по этому адресу;
  • указатель — это переменная, где хранится адрес ячейки памяти; он указывает на какую-то переменную, но обращается к ней не по имени, а по адресу.

Ситуация: Python меняет значения списка

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

Логика такая: когда нужно добавить нового пользователя, мы копируем содержимое этого шаблона в другую переменную, заполняем все данные, а потом добавляем её в массив пользователей:

-2

В консоли видно, что всё работает как нужно: пользователь добавлен в массив, а значит, можно продолжать писать код. Добавим таким же способом второго пользователя:

-3

Странно, но вместо того, чтобы добавить второго пользователя, мы получили в массиве две одинаковые записи. Как это может быть, если мы просто добавили новый элемент командой append () и ничего больше с массивом не делали?

Причина: в массив добавляются не элементы, а ссылки на них

Ссылки — это те же самые указатели, только привязанные к конкретной переменной. В нашем случае логика компьютера такая:

  1. Когда мы объявляем словарь user_info = {'Name':'','Work':''}, то компьютер создаёт новую переменную, выделяет под неё память и кладёт туда стартовое значение.
  2. Когда мы переменной new_user присваиваем значение user_info, то компьютер, чтобы не тратить память зря, кладёт в new_user не значение другой переменной, а ссылку на неё.
  3. Дальше мы начинаем заполнять переменную new_user, и компьютер понимает, что ссылка тут не подходит, нужно работать со значением. В этот момент он всё-таки выделяет для неё новую область памяти и кладёт туда значение из user_info — словарь {'Name':'','Work':''}.
  4. А когда мы добавляем эту переменную как элемент списка, то компьютер снова для экономии памяти отправляет в список не значение, а ссылку на переменную new_user. В итоге в первом элементе списка лежит не переменная, а ссылка.
  5. При выводе списка на экран компьютер по ссылке находит переменную new_user, берёт её значение и подставляет в массив — так мы видим на экране как бы правильную запись.

Когда мы добавляем второго пользователя, то компьютер делает всё точно так же: меняет значение переменной new_user и добавляет в массив ссылку на неё, тоже для экономии памяти.

В итоге в users вместо конкретных значений лежат две ссылки на одну и ту же переменную, в которой хранится только последнее значение. Со стороны кажется, что компьютер сам всё поменял, но на самом деле он сделал ровно то, что от него просили (как ему кажется).

-4

Что делать

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

  1. Создавать нового пользователя не из шаблона, а напрямую, подставив значение словаря: new_user = {'Name':'','Work':''}. В этом случае компьютер сразу выделит память для значения и будет работать с ним.
  2. Подключить модуль copy и использовать команду deepcopy — она принудительно скопирует в новую переменную не ссылку, а значение: new_user = copy.deepcopy(user_info).

-5