1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 2 из 7

Списки, срезы и строки: массив и его подвохи

Коротко

Список Python - это массив: ссылки на элементы лежат подряд, и a[i] вычисляется арифметикой за один шаг. Номера идут с нуля, отрицательные считаются с конца: a[-1] - последний. Срез a[i:j] берёт элементы с i по j - 1 и создаёт НОВЫЙ список, то есть копирует; range(i, j) устроен так же - правая граница не входит. b = a не копирует список, а даёт ему второе имя, поэтому b.append меняет и a. Строки неизменяемы: s[0] = "x" - ошибка, а s + t и s.upper() возвращают результат, не трогая s.

Если ты знаешь, почему [[0] * 3] * 3 - ловушка и что вернёт a[i - 1] при i = 0, листай до «Теперь сам» в конце.

Как думаешь, что окажется в списке a

a = [1, 2, 3]
b = a
b.append(4)
print(a)

Ответь себе до того, как читать дальше. В языках, где массив копируется при присваивании, ответ [1, 2, 3]. В Python - [1, 2, 3, 4], и эта разница стоит за половиной загадочных ошибок в программах со списками.

На что это похоже

Список - это ряд пронумерованных ячеек камеры хранения, а имя переменной - номерок от ряда. b = a выдаёт второй номерок от того же ряда: кто бы из двоих ни положил вещь в ячейку, второй её увидит. Срез a[:] - это новый ряд ячеек, куда переложили копии вещей. Номер ячейки начинается с нуля, потому что это не «какая по счёту», а «на сколько ячеек отступить от начала ряда».

Механизм: массив, имена и копии

Массив - это элементы одного размера, лежащие в памяти подряд. Адрес элемента номер i равен адресу начала плюс i, умноженному на размер элемента, - поэтому обращение по номеру стоит один шаг при любой длине. Отсюда и нумерация с нуля: первый элемент лежит на отступе ноль. Список Python - массив ссылок на значения, у которого в конце оставлен запас: append обычно кладёт в запас, а pop(0) (убрать первый элемент) или insert(0, x) (вставить x в начало) сдвигают весь хвост.

psevdonim.py
a = [1, 2, 3]
b = a
b.append(4)
c = a[:]
c.append(5)
print(a, b, c)
print(b is a, c is a)
↳  выводpython3 psevdonim.py
[1, 2, 3, 4] [1, 2, 3, 4] [1, 2, 3, 4, 5]
True False

is отвечает на вопрос «это один и тот же объект?». b - тот же список под другим именем, c - копия. Копия неглубокая: новый только внешний список, а если его элементы - сами списки, они общие у копии и оригинала.

Правило

Номера идут с нуля, a[-k] - k-й с конца. Срез a[i:j] и range(i, j) включают левую границу и не включают правую; если 0 ≤ i ≤ j ≤ len(a), длина среза - j - i. Срез копирует. Присваивание b = a не копирует, а даёт второе имя. Строки менять нельзя: операции над строкой возвращают результат, а исходную не трогают.

Разбор: срезы и граница, которая не входит

srezy.py
a = [10, 20, 30, 40, 50]
print(a[1:3], a[:2], a[3:], a[-1], a[-2:])
print(a[::-1], a[1:100], len(a[2:2]))
k = 2
print(a[:k] + a[k:] == a)
print(list(range(2, 5)), list(range(5, 0, -2)))
i = 0
print(a[i - 1])
↳  выводpython3 srezy.py
[20, 30] [10, 20] [40, 50] 50 [40, 50]
[50, 40, 30, 20, 10] [20, 30, 40, 50] 0
True
[2, 3, 4] [5, 3, 1]
50

Почему правая граница не входит, видно в третьей строке: a[:k] + a[k:] склеивается обратно в a без дыр и без повтора при любом k. Если бы граница входила, элемент номер k попал бы в обе половины. Срез за пределы списка (a[1:100]) не падает, а обрезается по концу - поэтому в нём четыре элемента, а не 100 - 1. Третий параметр - шаг: a[::-1] идёт с конца, range(5, 0, -2) считает вниз.

Последняя строка - самая опасная. Программа хотела взять «предыдущий элемент» и при i = 0 получила последний. Ошибки нет, исключения нет, есть просто неверное число. В языке, где выход за границу роняет программу, такой дефект нашёлся бы сразу; здесь его ищут трассировкой.

Разбор: таблица, у которой все строки одинаковые

Таблицу три на три нулей часто создают так:

setka.py
plohaya = [[0] * 3] * 3
plohaya[0][0] = 1
horoshaya = []
for _ in range(3):
    horoshaya.append([0] * 3)
horoshaya[0][0] = 1
print(plohaya)
print(horoshaya)
↳  выводpython3 setka.py
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]

[0] * 3 создаёт строку из трёх нулей. [строка] * 3 - список из трёх ссылок на ОДНУ и ту же строку, как b = a три раза подряд. Изменение одной клетки видно во всех строках. Цикл создаёт три разных строки. Этот дефект особенно коварен в динамическом программировании, где таблица - главная структура: ответ выглядит правдоподобно и неверен.

Что ломается без этого

Строки неизменяемы, и это видно и в ошибках, и в цене:

stroki.py
s = "алгоритм"
print(s[0], s[-1], s[2:5], len(s))
try:
    s[0] = "А"
except TypeError as e:
    print("ошибка:", e)
print(s.upper(), "-".join(["a", "b", "c"]), "a,b,,c".split(","))
↳  выводpython3 stroki.py
а м гор 8
ошибка: 'str' object does not support item assignment
АЛГОРИТМ a-b-c ['a', 'b', '', 'c']

Запись try: ... except TypeError as e: - перехват ошибки: если внутри try случилась ошибка этого типа, программа не останавливается, а переходит в блок except, и e - описание ошибки. split(",") режет строку по запятым, и между двумя запятыми подряд получается пустая строка ''. s.upper() не меняет s, а возвращает новую строку. Склейка itog = itog + slovo в цикле может каждый раз копировать всю строку - об этой цене глава «Задача и модель стоимости»; собирают куски в список и склеивают один раз через join. И ещё одно: удалять из списка внутри for по этому же списку нельзя. Цикл идёт по номерам, после удаления элементы сдвигаются, и следующий элемент перепрыгивается молча.

Зачем это в работе

Функция получила список, «на минутку» отсортировала его для своей проверки, и у вызывающего кода поменялся порядок данных - потому что передаётся не копия, а сам список. Кеш, в котором лежит ссылка на изменяемый список, внезапно «портится» сам собой. Срез a[1:] в рекурсии «обработай первый и вызови себя на хвосте» каждый раз копирует хвост и превращает линейный проход в квадратичный. Все три ситуации находятся одним вопросом: здесь копия или второе имя?

Теперь сам

Что напечатает программа?

a = [5, 6, 7]
b = a[:]
c = a
c[0] = 0
b[1] = 9
print(a, b, a[-1:], a[1:1])

Ответ. [0, 6, 7] [5, 9, 7] [7] []. c - второе имя для a, поэтому c[0] = 0 видно в a. b - копия, её изменение a не трогает. a[-1:] - срез от последнего до конца, то есть список из одного элемента, а не сам элемент. a[1:1] - пустой: левая граница совпала с правой, а правая не входит.

Главное

Список - массив с доступом по номеру за один шаг, а имя - лишь ссылка на него: b = a не копирует, а срез копирует всегда. Правая граница среза и range не входит - именно поэтому половинки a[:k] и a[k:] склеиваются без дыр и повторов.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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