Как учиться

Списки и массивы

Бумажный список кружка выглядит так: Аня — первая, Борис — вторая, Вера — третья, Глеб — четвёртый. Спросите «кто третий?», и любой ответит «Вера». Программа с тем же списком отвечает иначе: Вера у неё стоит на месте с номером два. Это не ошибка и не прихоть, а договорённость, на которой построена вся эта тема, и почти каждая первая ошибка со списками происходит из-за расхождения между человеческим и машинным счётом. В порядке школьной информатики списки идут после циклов: без цикла список остаётся неудобной записной книжкой.

Задача: какая по счёту контрольная прошла хуже всего

За четверть у ученика пять контрольных, оценки по порядку: grades = [4, 5, 3, 5, 4]. Нужно вывести, какая по счёту контрольная оказалась самой неудачной, и вывести так, как сказал бы человек: «третья».

Решаем в три действия. Находим самую низкую оценку: worst = min(grades) даёт 3. Ищем, на каком месте она стоит: pos = grades.index(worst) даёт 2. Печатаем для человека, прибавив единицу: print(pos + 1) выводит 3.

Каждое из трёх действий понятно на глаз, а вот последнее объясняет весь предмет: число, которое вернул index, — это номер позиции, отсчитанный от нуля, и порядковым номером для человека он ещё не стал. Человеку надо отдать число на единицу больше. Забыть про это прибавление и получить «вторая» вместо «третьей» — ровно та ошибка, которая встречается особенно часто.

Осталось уточнение, которое в условии задачи обычно спрятано: а если две контрольные одинаково плохие, например [4, 3, 5, 3]? Метод index вернёт только первую из них, то есть вторую по счёту, и про четвёртую программа промолчит. Если нужны все, придётся пройти по списку циклом и проверить каждый элемент. Решение небольшое, но оно показывает, что вопрос к задаче («какая» или «все такие») надо задавать до того, как пишешь код.

Откуда берётся нуль

Объяснение, которое помогает запомнить: номер показывает, сколько элементов нужно пропустить от начала, чтобы дойти до нужного. К первому пропускать нечего, поэтому его номер ноль; ко второму надо пропустить один, поэтому один; и так далее. Если в списке пять элементов, их номера идут от 0 до 4, и последний на единицу меньше длины.

Родителям и ученикам, у которых это не укладывается в голове, помогает простое упражнение: нарисовать список на бумаге клеточками, сверху над каждой подписать номер, а внутри записать значение. Номера и значения в этих двух рядах не совпадают, и именно это несовпадение надо увидеть глазами, один раз. После этого вопрос «какой номер у пятой оценки» уже не вызывает паузы.

Из этого вытекают две вещи. Первая: функция len(grades) возвращает 5, но элемента с номером 5 нет, и grades[5] остановит программу с ошибкой IndexError: list index out of range. Выход за границу — одна из самых частых ошибок наряду с путаницей в счёте. Вторая: последний элемент можно получить двумя способами, grades[4] и grades[-1], и второй удобнее, потому что не зависит от длины. Отрицательный номер считает с конца, от минус единицы.

Один раз, пока ничто не горит, стоит вызвать IndexError специально: написать print(grades[5]) на списке из пяти оценок и прочитать сообщение до конца. Последняя строка вывода говорит, что случилось, а строка над ней показывает, в каком месте программы. Ученики, которые этого не делали, видят в красном тексте угрозу и бросают читать; те, кто делал, видят адрес ошибки. Привычка читать сообщения снизу вверх экономит много времени, и репетитор по присланному файлу найдёт ошибку быстрее, если вы приложите это сообщение.

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

Отобрать нужное и посчитать

Оценки в задачах обычно приходят строкой с пробелами, например «4 5 3 5 4». Превратить её в список целых чисел можно записью grades = list(map(int, input().split())); та же заготовка, что в статье про переменные и типы данных, теперь получает смысл: split режет строку на части, int превращает каждую часть в число, list собирает результат в список.

Дальше типичные вопросы к списку. Сколько пятёрок: grades.count(5) даёт 2. Какие оценки не ниже четырёх: good = [g for g in grades if g >= 4] собирает новый список [4, 5, 5, 4]. В обычной записи это цикл с условием внутри и append, и сокращённая форма всего лишь короче, так что в начале можно писать цикл. Для проверки, не пуст ли результат, пригодится len(good): на пустом списке min и max падают с ошибкой ValueError, и это частый сюрприз, когда отбор ничего не нашёл. Сортировка списка и поиск в нём устроены отдельной темой и разобраны в статье про сортировки и поиск.

Список участников и срезы

Теперь другой набор: names = ["Аня", "Борис", "Вера", "Глеб"]. Запись names[2] даст «Вера», та самая третья по списку. А выражение names[1:3] даст двух человек, «Борис» и «Вера», и здесь повторяется знакомое из циклов правило: левая граница входит, правая нет. Если границу не писать вовсе, names[:2] берёт первых двух, а names[2:] всех, начиная с третьего.

Печатать список для человека, с единицы, можно двумя способами. Длинный: for i in range(len(names)): print(i + 1, names[i]). Короткий, через функцию enumerate: for i, name in enumerate(names, 1): print(i, name). Она отдаёт пары из номера и значения и принимает число, с которого вести счёт. Короткий вариант читается лучше, и привыкнуть к нему стоит сразу: «плюс один» в длинном легко забыть.

Добавить, удалить, изменить

Список отличается от строки тем, что его можно менять на месте. Метод names.append("Дима") добавляет в конец. Метод names.remove("Борис") убирает первое найденное «Борис». Присваивание names[0] = "Алина" заменяет первого участника. Всё это работает потому, что список хранится как место в памяти с содержимым, и из этого следует неочевидное поведение.

Попробуйте: a = [1, 2], потом b = a, потом b.append(3), потом print(a). Вы увидите [1, 2, 3], хотя к a ничего не добавляли. Строка b = a дала тому же списку второе имя, и копии никакой нет. Настоящая копия делается явно: b = a.copy() или b = a[:]. Эта ловушка встречается и в школьных задачах: изменили b, а поменялся a.

Когда списка уже мало

Участникам кружка соответствуют оценки: у Ани четвёрка, у Бориса пятёрка. Первая мысль — два списка, names и grades, где номера совпадают: names[1] это Борис, grades[1] его оценка. Работает, пока списки не расходятся. Стоит удалить Бориса из одного и забыть про другой, как оценки поедут и достанутся другим людям, а программа не скажет ни слова.

В таких задачах, где у каждого значения есть имя, список перестаёт быть подходящим, и нужен другой способ хранения: пары «ключ и значение» в словаре. О нём подробно в статье про словари и множества. Признак, что пора переходить, простой: если вы ведёте два списка и следите, чтобы номера совпадали, список тут лишний.

В школьных учебниках чаще всего говорят «массив» и объясняют его как таблицу значений одного типа, у которой размер задаётся заранее: так устроены Pascal и обычные массивы C++, и в Pascal границы можно выбрать самому, так что счёт там бывает и с единицы. В Python вместо этого есть список, который растёт и принимает значения любого типа. Для школьных задач разница почти не чувствуется, и я бы не заставлял ребёнка заучивать её, пока он пишет на Python. Она всплывает при переходе на C++: выход за границу там может не остановить программу сообщением и молча прочитать чужую память. Тут Python прощает куда больше, и это стоит иметь в виду тем, кто выбирает язык для экзамена; сравнение вариантов — в статье о структуре ЕГЭ по информатике.

Где это встретится дальше

Строка по устройству напоминает список символов: у неё те же номера от нуля, те же срезы, и всё сказанное об этом пригодится в статье про строки и работу с текстом. Когда действие со списком надо повторять в разных местах, его выносят в отдельную функцию, и тогда встаёт вопрос, что именно получает функция: сам список или его копию; ответ связан с тем, что вы только что прочитали про присваивание, и разобран в статье про функции и аргументы.

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

Начать поиск:

Вопросы и ответы

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

Как не выйти за границу списка? Помнить, что последний номер на единицу меньше длины, а при переборе циклом использовать range(len(grades)) или идти прямо по элементам, не трогая номера: for g in grades. Во втором случае выйти за границу невозможно вообще, и поэтому он предпочтительнее, когда номер не нужен.

Список и массив — это одно и то же? Близкие вещи под разными именами: массив в школьных языках жёстче, список в Python гибче. Подробнее — в разделе выше; на школьных задачах разницу почти не видно.

Оставить заявку

Ещё в рубрике «Как учиться»