Генерация подсписок с использованием умножения (*) непредвиденного поведения

Я уверен, что на это был дан ответ, но я не был уверен, как его описать.

Скажем, я хочу создать список, содержащий 3 пустых списка, например:

lst = [[], [], []]

Я думал, что все умнее, делая это:

lst = [[]] * 3

Но я обнаружил, что после отладки какого-то странного поведения это вызвало обновление одного из подписок, например lst[0].append(3), для обновления всего списка, сделав его [[3], [3], [3]], а не [[3], [], []].

Однако, если я инициализирую список с помощью

lst = [[] for i in range(3)]

тогда выполнение lst[1].append(5) дает ожидаемый [[], [5], []]

Мой вопрос , почему это происходит? Интересно отметить, что если я делаю

lst = [[]]*3
lst[0] = [5]
lst[0].append(3)

тогда "связь" ячейки 0 сломается, и я получаю [[5,3],[],[]], но lst[1].append(0) все еще вызывает [[5,3],[0],[0].

Мое лучшее предположение заключается в том, что использование умножения в форме [[]]*x заставляет Python хранить ссылку на одну ячейку...?

Ответ 1

Мое лучшее предположение заключается в том, что использование умножения в форме [[]] * x заставляет Python хранить ссылку на одну ячейку...?

Да. И вы можете проверить это самостоятельно

>>> lst = [[]] * 3
>>> print [id(x) for x in lst]
[11124864, 11124864, 11124864]

Это показывает, что все три ссылки относятся к одному и тому же объекту. И обратите внимание, что действительно имеет смысл, что это происходит 1. Он просто копирует значения, и в этом случае значения являются ссылками. И поэтому вы видите одну и ту же рекомендацию три раза.

Интересно отметить, что если я делаю

lst = [[]]*3
lst[0] = [5]
lst[0].append(3)

тогда "связь" ячейки 0 прерывается, и я получаю [[5,3],[],[]], но lst[1].append(0) все еще вызывает [[5,3],[0],[0].

Вы изменили ссылку, которая занимает lst[0]; то есть вы назначили новое значение lst[0]. Но вы не изменили значение других элементов, они все еще ссылаются на тот же объект, о котором они говорили. И lst[1] и lst[2] по-прежнему относятся к одному и тому же экземпляру, поэтому, конечно, добавление элемента в lst[1] приводит к тому, что lst[2] также видит это изменение.

Это классическая ошибка, которую люди делают с указателями и ссылками. Здесь простая аналогия. У вас есть лист бумаги. На этом вы пишете адрес кого-то дома. Теперь вы берете этот лист бумаги и копируете его дважды, так что в итоге вы получаете три листа бумаги с тем же адресом, написанным на них. Теперь возьмите первый лист бумаги, напишите адрес, написанный на нем, и напишите новый адрес в чужой дом. Изменился ли адрес, записанный на двух других листах бумаги? Нет. Это именно то, что сделал ваш код. Вот почему другие два элемента не меняются. Далее, представьте, что владелец дома с адресом, который все еще находится на втором листе бумаги, создает дополнительный гараж для их дома. Теперь я спрашиваю вас, имеет ли дом, чей адрес находится на третьем листе бумаги, дополнительный гараж? Да, это так, потому что это точно тот же дом, что и тот, чей адрес написан на втором листе бумаги. Все это объясняет ваш второй пример кода.

1: Вы не ожидали, что Python вызовет "конструктор копирования", не так ли? Пук.

Ответ 2

Это потому, что умножение последовательности просто повторяет ссылки. Когда вы пишете [[]] * 2, вы создаете новый список с двумя элементами, но оба эти элемента являются одним и тем же объектом в памяти, а именно пустым списком. Следовательно, изменение в одном отражается в другом. Понимание, напротив, создает на каждом итерации новый независимый список:

>>> l1 = [[]] * 2
>>> l2 = [[] for _ in xrange(2)]
>>> l1[0] is l1[1]
True
>>> l2[0] is l2[1]
False

Ответ 3

Они ссылаются на одни и те же списки.

Здесь есть похожие вопросы и здесь

И из FAQ:

"* не создает копии, он только создает ссылки на существующие объекты".

Ответ 4

Предположим, что использование умножения в форме [[]] * x заставляет Python сохранять ссылку на одну ячейку правильно.

Итак, вы получите список из 3 ссылок на один и тот же список.

Ответ 5

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

>>> a = []
>>> b = [a]
>>> c = b * 3  # c now contains three references to a
>>> d = [ a for _ in xrange(4) ]  # and d contains four references to a
>>> print c
[[], [], []]
>>> print d
[[], [], [], []]
>>> a.append(3)
>>> print c
[[3], [3], [3]]
>>> print d
[[3], [3], [3], [3]]
>>> x = [[]] * 3  # shorthand equivalent to c
>>> print x
[[], [], []]
>>> x[0].append(3)
>>> print x
[[3], [3], [3]]

Вышеупомянутое эквивалентно вашему первому примеру. Теперь, когда каждому списку дается собственная переменная, мы надеемся, что более понятно почему. c[0] is c[1] будет оцениваться как True, потому что оба выражения оцениваются одним и тем же объектом (a).

Второй пример создает несколько разных объектов внутреннего списка.

>>> c = [[], [], []]  # this line creates four different lists
>>> d = [ [] for _ in xrange(3) ]  # so does this line
>>> c[0].append(4)
>>> d[0].append(5)
>>> print c
[[4], [], []]
>>> print d
[[5], [], []]