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

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

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

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

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

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

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

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

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

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

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

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

# шаблон для всех пользователей
user_info = {'Name':'','Work':''}
# на старте массив с пользователями пуст
users = []
# создаём нового пользователя
new_user = user_info
# заполняем имя и кем работает
new_user['Name'] = 'Максим'
new_user['Work'] = 'главред'
# добавляем нового пользователя в массив
users.append(new_user)
# выводим всех пользователей на экран
print(users)
Что делать, когда Python сам меняет значения списка

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

# шаблон для всех пользователей
user_info = {'Name':'','Work':''}
# на старте массив с пользователями пуст
users = []
# создаём нового пользователя
new_user = user_info
# заполняем имя и кем работает
new_user['Name'] = 'Максим'
new_user['Work'] = 'главред'
# добавляем нового пользователя в массив
users.append(new_user)
# выводим всех пользователей на экран
print(users)

# точно так же добавляем второго пользователя 
new_user = user_info
# заполняем его данные
new_user['Name'] = 'Михаил'
new_user['Work'] = 'автор'
# и тоже добавляем в массив
users.append(new_user)
# выводим список всех пользователей на экран
print(users)
Что делать, когда Python сам меняет значения списка

Странно, но вместо того, чтобы добавить второго пользователя, мы получили в массиве две одинаковые записи. Как это может быть, если мы просто добавили новый элемент командой 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 вместо конкретных значений лежат две ссылки на одну и ту же переменную, в которой хранится только последнее значение. Со стороны кажется, что компьютер сам всё поменял, но на самом деле он сделал ровно то, что от него просили (как ему кажется).

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

Что делать

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

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

# подключаем модуль для копирования
import copy
# шаблон для всех пользователей
user_info = {'Name':'','Work':''}
# на старте массив с пользователями пуст
users = []
# создаём нового пользователя, копируя значение из шаблона
new_user = copy.deepcopy(user_info)
# заполняем имя и кем работает
new_user['Name'] = 'Максим'
new_user['Work'] = 'главред'
# добавляем нового пользователя в массив
users.append(new_user)
# выводим всех пользователей на экран
print(users)

# точно так же добавляем второго пользователя 
new_user = copy.deepcopy(user_info)
# заполняем его данные
new_user['Name'] = 'Михаил'
new_user['Work'] = 'автор'
# и тоже добавляем в массив
users.append(new_user)
# выводим список всех пользователей на экран
print(users)
Что делать, когда Python сам меняет значения списка

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Любишь Python? Зарабатывай на нём!
Изучите самый модный язык программирования и станьте крутым бэкенд-разработчиком. Старт — бесплатно.
Попробовать
Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём!
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium