МНЕНИЕ

Не используйте умножение списка Python ([n] * N)

Это ловушка

Независимо от того, являетесь ли вы новичком или опытным программистом на Python, скорее всего, вы либо использовали умножение списков, либо читали об этом в статьях в стиле «классные функции Python».

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

Преимущество умножения списка в том, что оно абстрагирует процесс инициализации списка.

Вместо использования итеративного подхода или понимания списка:

# Iterative approach
my_list = []
for _ in range(N):
    my_list.append(n)
# List comprehension
my_list = [n for _in range(N)]

вы можете получить тот же результат с помощью:

my_list = [n] * N

Видите ли, я изучал языки программирования в следующем порядке:

C -> C++ -> MATLAB -> R -> Python.

Ни один другой язык программирования, с которым я работал до Python, не мог даже отдаленно обеспечить такую ​​лаконичность и интуитивность.

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

Я помню, как однажды я потратил целый день на отладку кода только для того, чтобы выяснить, что проблема возникла из-за неправильного создания списка с использованием оператора *.

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

Что не так с умножением списка

Рассмотрим следующий код:

>>> my_list = [0] * 3
>>> my_list[0] = 1
>>> my_list
[1, 0, 0]

Это то, что вы ожидаете. Все идет нормально.

Теперь давайте попробуем создать 2D-массив, используя тот же подход:

>>> my_list = [[0] * 3] * 5
>>> my_list[0][0] = 1
>>> my_list
[[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]

Хм! Это, вероятно, не то, что вы хотели бы.

Как насчет того, чтобы продвинуть его дальше, инициализировав 3D-массив:

>>> my_list = [[[0] * 3] * 5] * 2
>>> my_list[0][0][0] = 1
>>> my_list
[[[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]], [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]]

Ожидаемым результатом будет обновление первого значения подсписка [0, 0, 0]. Однако похоже, что обновление было реплицировано во всех подсписках.

Итак, почему это произошло?

Как работает умножение списков?

Чтобы понять предыдущее поведение, стоит вернуться к FAQ Python, в котором говорится:

«Причина в том, что репликация списка с помощью * не создает копии, а создает только ссылки на существующие объекты».

Давайте переведем это в код, чтобы еще лучше понять, как Python работает под капотом:

  • Умножение списка:
my_list = [[0] * 5] * 5
for i in range(5):
    print(id(my_list[i]))

Выход:

2743091947456
2743091947456
2743091947456
2743091947456
2743091947456
  • Использование циклов for
my_list = []
for _ in range(5):
    my_list.append([0] * 5)
for i in range(5):
    print(id(my_list[i]))
print(my_list)

Выход:

2743091947456
2743095534208
2743095532416
2743095534336
2743095532288
  • Интерпретация

В отличие от цикла for, все списки, скопированные через оператор *, указывают на один и тот же адрес памяти. Это означает, что любое изменение, влияющее на один вложенный список, влияет на все остальные, что явно противоречит нашему первоначальному замыслу.

Теперь возникает вопрос:

  • Почему первый пример ([n] * N) работает просто отлично, хотя все элементы списка ссылаются на один и тот же объект?

Оказывается, причина такого поведения (как указано в викибуке Python) заключается в том, что списки — изменяемые элементы, а int, str и им подобные — неизменяемые. Проверьте это:

А поскольку неизменяемые объекты нельзя изменить, Python создает новую (другую) ссылку на объект при обновлении элемента в списке.

>>> my_list = [0] * 3
>>> id(my_list[0])
1271862264016
>>> my_list[0] = 1
>>> id(my_list[0])
1271862264048

Обходной путь

Быстрое и простое решение этой проблемы — использование понимания списка. Это, конечно, помимо стандартного цикла for.

>>> my_list = [[0] * 3 for _ in range(5)]
>>> my_list[0][0] = 1
>>> my_list
[[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

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

>>> my_list = [[0] * 3 for _ in range(5)]
>>> [id(l) for l in my_list]
[1271867906112, 1271864321536, 1271864322048, 1271864326912, 1271864322560]

Что еще более важно, этот подход отлично работает во всех сценариях.

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

Заключение

Я не большой фанат синтаксического сахара Pythonic.

Да, я согласен, что это делает код компактным и лаконичным.

Однако когда conciseness == readability работал в индустрии программного обеспечения?

На самом деле, чем больше я программирую на Python, тем больше я склоняюсь к использованию стандартного синтаксиса Python и отказу от сокращений.

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

И если вы не можете обойтись без сокращений, по крайней мере, прочитайте, что хорошего, что плохого, что уродливого в синтаксическом сахаре, который вы используете. В этом случае может потребоваться умеренное понимание концепций разработки программного обеспечения (например, структур данных, распределения памяти и т. д.).

Удачного кодирования!

Если вы сочтете это полезным, подумайте о том, чтобы стать премиум-участником. Если вы воспользуетесь этой ссылкой, я получу небольшую долю.