Перейти к содержимому
Теория · Списки · урок 6 из 11

Вложенные списки и копии

Представьте кинозал. В нём несколько рядов, и в каждом ряду — несколько кресел. Чтобы найти своё место, вы сначала ищете ряд, а потом кресло в этом ряду.

Таблицу — места в зале, клетки игрового поля — в Python хранят так же: как список рядов, где каждый ряд — тоже список. Такой список называют вложенным:

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]]

Давайте разберём по строкам:

  1. len(grid) — число рядов, 2, а len(grid[0]) — число кресел в первом ряду, 3.
  2. grid[1] даёт второй ряд целиком: [4, 5, 6].
  3. grid[1][0] читается слева направо: сначала второй ряд, потом первое кресло в нём — 4.
  4. grid[0][2] = 9 так же находит первый ряд, а в нём заменяет третий элемент.

Обход двумя циклами

Чтобы обойти все кресла зала, нужны два цикла, один внутри другого. Внешний цикл идёт по рядам, а внутренний — по креслам текущего ряда:

Python
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. Внешний цикл берёт первый ряд, [1, 2, 3], и связывает с ним имя row.
  2. Внутренний цикл проходит этот ряд целиком: печатает 1 2 3 и прибавляет каждое число к total.
  3. Внешний цикл берёт второй ряд, и внутренний снова проходит его от начала до конца: 4 5 6.
  4. После обоих циклов print() завершает строку, а последний print выводит сумму всех чисел — 21.

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

Поверхностная и глубокая копия

a[:], list(a) и a.copy() копируют только внешний список. Для таблицы это значит, что ряды копия делит с оригиналом. Строка import copy подключает набор функций для копирования, как import math — математические. Копии бывают двух видов.

Поверхностная копия

Поверхностная копия — новый внешний список, в котором лежат те же самые ряды, что в оригинале. Её делают copy.copy(grid) и все три способа выше. Представьте второй вход в тот же зал: дверь новая, а ряды за ней прежние.

Глубокая копия

Глубокую копию делает copy.deepcopy(grid): в ней скопированы и внешний список, и все вложенные. Это второй зал, построенный по тому же плану, со своими рядами и креслами.

Python
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]. Если так же умножить и внешний список, ряды окажутся одним объектом:

Python
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 и подпишись на канал — откроются все уроки.

Войти другим способом