Изменение списка с помощью цикла

За пример возьмем такой код:

sp = [1, [2], 3]
for i in sp:
    i *= 2
    print(i)
print(sp)

Результатом он даст:

2
[2, 2]
6
[1, [2, 2], 3]

Сам вопрос: Почему меняется элемент списка sp без прямого обращения, если значением этого элемента является список?

Итератор i лишь принимает значение очередного элемента списка sp, и при взаимодействии с ним значения элементов изначального списка меняться не должны, что мы и видим на примере чисел. Но, если элементом списка sp является подсписок, то картина совершенно иная.


Ответы (2 шт):

Автор решения: Андрей Осинцев

Давайте разберём ваш код.

Вы проходите список

sp = [1, [2], 3]
for i in sp:
    i *= 2

Прежде всего. Давайте сразу определимся, что существуют значимые и ссылочные типы данных.

Значимые (int, float, double, boolean и т.д.)

Ссылочные (разнообразные структуры данных, в том числе список, list)

Когда вы проходите список циклом for, в зависимости от того, на какой именно элемент в списке указывает указатель i он ведёт себя по разному.

В случае с числами, внутри цикла for i становится локальной (внутри цикла) копией числа, взятого из списка. Когда вы умножаете i, вы умножаете копию. Действительно, выводя её на экран изнутри цикла вы получите умноженное значение, но это не значение внутри списка, это копия.

В случае со списком внутри списка, i указывает на объект, и последующая операция умножения применяется к объекту, который, соответственно изменяется. В данном случае i - это не локальная копия, i - это указатель, на тот самый "подсписок"

Если вы хотите поэлементно умножить каждый элемент списка, обращайтесь к нему по индексу.

for i in range(len(sp)): 
    sp[i] *= 2

Успехов.

→ Ссылка
Автор решения: CrazyElf

Тут на самом деле есть две тонкости:

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

Давайте добавим в ваш код печать адреса переменной i до изменения и после изменения:

sp = [1, [2], 3]
for i in sp:
    id1 = id(i)
    i *= 2
    id2 = id(i)
    print(id1, '==' if id1 == id2 else "!=", id2, i)
print(sp)

Вывод:

139686891790624 != 139686891790656 2
139686765180672 == 139686765180672 [2, 2]
139686891790688 != 139686891790784 6
[1, [2, 2], 3]

Видите? Адрес числовых переменных меняется - теперь это новые объекты, не имеющие больше связи с теми объектами, которые были в списке. А вот список, вложенный в список, сохранил свою идентичность - это тот же самый объект, но уже изменённый.

  1. Есть некоторая разница между оператором, используемым в разных формах.

В "полной форме":

переменная = переменная оператор значение

В "сокращённой" форме:

переменная оператор= значение

Давайте поменяем в вашем коде i *= 2 на i = i * 2, что же будет на выходе тогда:

139686891790624 != 139686891790656 2
139686546719168 != 139686526698432 [2, 2]
139686891790688 != 139686891790784 6
[1, [2], 3]

В данном случае операция со списком не выполнилась "по месту", а был создан новый список, ссылка на который присвоилась в переменную i, и этот список потерял связь с нашим списком! Так что эти два оператора = * и *= оказались не совсем тождественными, об этом нужно помнить.

→ Ссылка