Почему использование оператора умножения в списке создает список указателей?

>>> rows = [['']*5]*5
>>> rows
[['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', '']]
>>> rows[0][0] = 'x'

Естественно, я ожидаю, что строки станут:

[['x', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', '']]

Вместо этого я получаю:

[['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', '']]

Кажется, что элементы списка строк являются указателями на один и тот же старый список [''] * 5. Почему он работает таким образом и является ли это функцией Python?

Ответ 1

Поведение не характерно для оператора повторения (*). Например, если вы объединяете два списка с помощью +, поведение будет одинаковым:

In [1]: a = [[1]]

In [2]: b = a + a

In [3]: b
Out[3]: [[1], [1]]

In [4]: b[0][0] = 10

In [5]: b
Out[5]: [[10], [10]]

Это связано с тем, что списки - это объекты, а объекты хранятся по ссылке. Когда вы используете * et al, эта ссылка повторяется, следовательно, поведение, которое вы видите.

Ниже показано, что все элементы rows имеют одинаковый идентификатор (т.е. адрес памяти в CPython):

In [6]: rows = [['']*5]*5

In [7]: for row in rows:
   ...:     print id(row)
   ...:     
   ...:     
15975992
15975992
15975992
15975992
15975992

Следующий пример эквивалентен вашему примеру, за исключением того, что он создает пять различных списков для строк:

rows = [['']*5 for i in range(5)]

Ответ 2

Тот факт, что имена, функциональные параметры и контейнеры имеют ссылочную семантику, является очень простым дизайнерским решением в Python. Это влияет на то, как Python работает во многих аспектах, и вы выбрали только один из этих аспектов. Во многих случаях эталонная семантика более удобна, тогда как в других случаях копии были бы более удобными. В Python вы всегда можете явно создать копию, если это необходимо, или, в этом случае, вместо этого используйте понимание списка:

rows = [[''] * 5 for i in range(5)]

Вы могли бы разработать язык программирования с другой семантикой, и есть много языков, которые имеют разную семантику, а также языки с аналогичной семантикой. Почему это решение было принято, немного сложно ответить - язык просто должен иметь некоторую семантику, и вы всегда можете спросить, почему. Вы могли бы также спросить, почему Python динамически типизирован, и, в конце концов, ответ заключается в том, что именно это было решено Guido еще в 1989 году.

Ответ 3

Вы правы, что Python использует указатели "под капотом", и да, это особенность. Я не знаю точно, почему они это сделали way- Я предполагаю, что это было для скорости и сокращения использования памяти.

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