Вложенные списки и копии
Представьте кинозал. В нём несколько рядов, и в каждом ряду — несколько кресел. Чтобы найти своё место, вы сначала ищете ряд, а потом кресло в этом ряду.
Таблицу — места в зале, клетки игрового поля — в Python хранят так же: как список рядов, где каждый ряд — тоже список. Такой список называют вложенным:
grid = [[1, 2, 3], [4, 5, 6]]
print(len(grid), len(grid[0])) # 2 3
print(grid[1]) # [4, 5, 6]
print(grid[1][0]) # 4
grid[0][2] = 9
print(grid) # [[1, 2, 9], [4, 5, 6]]Давайте разберём по строкам:
len(grid)— число рядов,2, аlen(grid[0])— число кресел в первом ряду,3.grid[1]даёт второй ряд целиком:[4, 5, 6].grid[1][0]читается слева направо: сначала второй ряд, потом первое кресло в нём —4.grid[0][2] = 9так же находит первый ряд, а в нём заменяет третий элемент.
Обход двумя циклами
Чтобы обойти все кресла зала, нужны два цикла, один внутри другого. Внешний цикл идёт по рядам, а внутренний — по креслам текущего ряда:
grid = [[1, 2, 3], [4, 5, 6]]
total = 0
for row in grid:
for x in row:
print(x, end=" ") # 1 2 3 4 5 6
total += x
print()
print(total) # 21Разберём, в каком порядке работают циклы:
- Внешний цикл берёт первый ряд,
[1, 2, 3], и связывает с ним имяrow. - Внутренний цикл проходит этот ряд целиком: печатает
1 2 3и прибавляет каждое число кtotal. - Внешний цикл берёт второй ряд, и внутренний снова проходит его от начала до конца:
4 5 6. - После обоих циклов
print()завершает строку, а последнийprintвыводит сумму всех чисел —21.
Тело внутреннего цикла выполнилось шесть раз: по три раза на каждый из двух рядов. Так обходят любую таблицу: ряд за рядом, а в ряду — слева направо.
Поверхностная и глубокая копия
a[:], list(a) и a.copy() копируют только внешний список. Для таблицы это значит, что ряды копия делит с оригиналом. Строка import copy подключает набор функций для копирования, как import math — математические. Копии бывают двух видов.
Поверхностная копия
Поверхностная копия — новый внешний список, в котором лежат те же самые ряды, что в оригинале. Её делают copy.copy(grid) и все три способа выше. Представьте второй вход в тот же зал: дверь новая, а ряды за ней прежние.
Глубокая копия
Глубокую копию делает copy.deepcopy(grid): в ней скопированы и внешний список, и все вложенные. Это второй зал, построенный по тому же плану, со своими рядами и креслами.
import copy
grid = [[1, 2], [3, 4]]
shallow = copy.copy(grid)
deep = copy.deepcopy(grid)
grid[0][0] = 9
print(shallow[0], deep[0]) # [9, 2] [1, 2]
print(shallow[0] is grid[0]) # TrueРазберём, что изменилось после grid[0][0] = 9:
- в поверхностной копии первый ряд — тот же список, что
grid[0], поэтому в ней видно[9, 2]; shallow[0] is grid[0]даётTrue: это один объект;- в глубокой копии ряды свои, и в ней по-прежнему
[1, 2].
Глубокая копия нужна, когда в копии будут менять элементы рядов. Если меняют только внешний список — добавляют или убирают целые ряды, — хватает поверхностной.
Этот вопрос часто задают на собеседовании: «Чем поверхностная копия отличается от глубокой?» Ответ — в том, копируются ли вложенные объекты. Поверхностная копия делит их с оригиналом, а глубокая создаёт свои.
Частая ошибка
Таблицу нулей хочется собрать одной строкой. Оператор * повторяет элементы списка: [0] * 3 — это [0, 0, 0]. Если так же умножить и внешний список, ряды окажутся одним объектом:
grid = [[0] * 3] * 2 # ✗
grid[0][0] = 5
print(grid) # [[5, 0, 0], [5, 0, 0]]
grid = []
for i in range(2):
grid.append([0] * 3) # ✓
grid[0][0] = 5
print(grid) # [[5, 0, 0], [0, 0, 0]]Внешнее * 2 повторяет не ряд, а ссылку на него: в grid два элемента, и оба — один и тот же список. Это как два входа в зал, где всего один ряд. Поэтому изменение через grid[0] видно и в grid[1].
Внутреннее [0] * 3 безопасно: числа изменить нельзя, и grid[0][0] = 5 заменяет один элемент ряда новым объектом.
Исправление — во второй части примера: ряды создают циклом. На каждой итерации выражение [0] * 3 вычисляется заново и строит новый список-ряд, а append добавляет его во внешний список. Переменная i в теле не используется: она нужна, только чтобы тело выполнилось два раза.
Главное
- Таблицу хранят как список списков:
grid[1][0]— второй ряд, первый элемент. - Два вложенных цикла обходят таблицу ряд за рядом, а в ряду — слева направо.
- Поверхностная копия делит ряды с оригиналом, а
copy.deepcopyкопирует и их. [[0] * 3] * 2повторяет один и тот же ряд, поэтому ряды создают циклом.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.