Списки и циклы¶
Автор(ы)
Введение в списки объектов¶
В предыдущих лекциях операции были с малым количеством переменных. Для каждого блока логики или примера кода вводилось 3-5 объектов, над которыми осуществлялись некоторые операции. Но что делать, если объектов куда больше? Скажем, необходимо хранить информацию об учащихся класса – пусть это будет рост, оценка по математике или что-либо другое. Крайне неудобно будет создавать и хранить 30 отдельных переменных. А если ещё и нужно посчитать среднюю оценку в классе!
Такой код к тому же получается крайне негибким: если количество студентов, как и их состав, изменится, то нужно и формулу переписать, так ещё и делитель – в нашем случае 30 – изменять.
Часто в программах приходится работать с большим количеством однотипных переменных. Специально для этого придуманы массивы (по-английски array). В Python их ещё называют списками (list). В некоторых языках программирования эти понятия отличаются, но не в Python. Список может хранить переменные разного типа. Также списки называют «контейнерами», так как они хранят какой-то набор данных. Для создания простого списка необходимо указать квадратные скобки или вызвать конструктор типа (list
– это отдельный тип, фактически такой же, как int
или str
), а затем перечислить объекты через запятую:
Между тем
Хоть список и хранит переменные разного типа, но так делать без особой необходимости не рекомендуется – можно запутаться и ошибиться в обработке объектов списка. В большинстве других языков программирования массив может хранить только объекты одного типа.
Для хранения сложных структур (скажем, описание студента – это не только оценка по математике, но и фамилия, имя, адрес, рост и так далее) лучше использовать классы – с ними познакомимся в будущем. А ещё могут пригодиться кортежи, или tuple
.
Теперь можно один раз создать список и работать с ним как с единым целым. Да, по-прежнему для заведения оценок студентов придётся разово их зафиксировать, но потом куда проще исправлять и добавлять! Рассмотрим пример нахождения средней оценки группы, в которой всего 3 учащихся, но к ним присоединили ещё 2, а затем – целых 5:
В коде выше продемонстрировано сразу несколько важных аспектов:
- добавлять по одному объекту в конец списка можно с помощью метода списка
append()
. - метод
append()
принимает в качестве аргумента один объект. - слияние нескольких списков (конкатенация, прямо как при работе со строками) осуществляется командой
extend()
(расширить в переводе с английского). - для списков определена функция
len()
, которая возвращает целое числоint
– количество объектов в списке. - функция
sum()
может применяться к спискам для суммирования всех объектов (если позволяет тип – то есть дляfloat
,int
иbool
; попробуйте разобраться самостоятельно, как функция работает с последним указанным типом). - для методов
append()
иextend()
не нужно приравнивать результат выполнения какой-то переменной – изменится сам объект, у которого был вызван метод (в данном случае этоmath_journal()
); - списки в Python упорядочены, то есть объекты сами по себе места не меняют, и помнят, в каком порядке были добавлены в массив.
О методе
В тексте выше встречается термин метод, который, быть может, не знаком. По сути метод есть такая же функция, о которых говорили ранее, но она принадлежит какому-то объекту с определенным типом. Не переживайте, если что-то непонятно – про функции и методы поговорим подробно в ближайших лекциях!
print()
, sum()
– функции, они существуют сами по себе; append()
, extend()
– методы объектов класса list
, не могут использоваться без них.
Индексация списков¶
Теперь, когда стало понятно, с чем предстоит иметь дело, попробуем усложнить пример. Как узнать, какая оценка у третьего студента? Всё просто – нужно воспользоваться индексацией списка:
И снова непонятный пример! Давайте разбираться:
- для обращения к
i
-тому объекту нужно в квадратных скобках указать его индекс; - индекс в Python начинается c нуля – это самое важное и неочевидное, здесь чаще всего случаются ошибки;
- поэтому
[3]
обозначает взятие 4-й оценки (и потому выводится4
, а не3
); - всего оценок 5, но так как индексация начинается с нуля, то строчка
math_journal[5]
выведет ошибку – доступны лишь индексы[0, 1, 2, 3, 4]
для взятия (так называется процедура обращения к элементу списка по индексу – взятие по индексу).
Также в Python
существуют отрицательные индексы (-1, -2 ...). Они отсчитывают объекты списка, начиная с конца. Так как нуль уже занят (под первый объект), то он не используется.
Последняя оценка: 5
Предпоследняя оценка: 4
Последний студент сдал очень хорошо, на его фоне предпоследний просто двоечник!
Всё это важно не только для грамотного оперирования конкретными объектами, но и следующей темы.
Срезы¶
Срезы, или slices – это механизм обращения сразу к нескольким объектам списка. Для создания среза нужно в квадратных скобках указать двоеточие, слева от него – индекс начала среза (по умолчанию 0
, можно не выставлять) включительно, справа – границу среза не включительно (пустота означает «до конца списка»). Может показаться нелогичной такая разнородность указания границ, но на самом деле она безумно удобна – особенно вместе с тем, что индексация начинается с нуля. Быстрее объяснить на примере:
first_3_grades = [1, 2, 3]
last_2_grades = [4, 5]
some_slice = [2, 3, 4, 5]
Верно ли, что единица входит в some_slice? False
Верно ли, что единица входит в yet_another_slice? True
Между тем
Можно сделать пустой срез, и тогда Python вернет пустой список без объектов. Можете проверить сами: ["1", "2", "3"][10:20]
Давайте проговорим основные моменты, которые крайне важно понять:
- так как индексация начинается с нуля (значение по умолчанию) и правая граница не включается в срез, то берутся объекты с индексами
[0, 1, 2]
, что в точности равняется трём первым объектам; - срез
[-2:]
указывает на то, что нужно взять все объекты до конца, начиная с предпоследнего; - значения в срезе могут быть вычислимы (и задаваться сколь угодно сложной формулой), но должны оставаться целочисленными;
- если нужно взять
k
объектов, начиная сi
-го индекса, то достаточно в качестве конца среза указатьk+i
; - для проверки вхождения какого-либо объекта в список нужно использовать конструкцию
x_obj in some_list
, которая вернетTrue
, если массив содержитx_obj
, иFalse
в ином случае; - самый простой способ сделать копию списка - это сделать срез по всему объекту:
my_list[:]
. Однако будьте внимательны – в одних случаях копирование происходит полностью (по значению), а в некоторых сохраняются ссылки (то есть изменив один объект в скопированном списке вы измените объект в исходном). Связано это с типом объектов (mutable/immutable), подробнее об этом будет рассказано в следующей лекции. В общем, если работаете с простыми типами (int
/str
), то срез вернёт копию, и её изменение не затронет исходный список. Однако для хранения новых данных нужна память, поэтому при копировании десятков миллионов объектов можно получить ошибку, связанную с нехваткой памяти.
Кортеж (tuple)¶
Выше уже упоминалось о таком понятии как кортеж. Можете спросить: зачем нужны кортежи, если они так сильно похожи на списки? Чтобы больше не возникало подобных вопросов давайте сравним их!
- Главным отличием является то, что кортежи это неизменяемый тип данных. То есть в процессе выполнения программы можно быть уверенным, что значения внутри кортежа останутся неизменными. И из этого свойства вытекает множество других.
-
Кортеж имеет меньше размер, чем список. Это связано с тем, что список – изменяемый объект и ему нужно хранить дополнительную информацию о выделенной памяти.
-
Возможность использовать кортежи в качестве ключей словаря:
Чтобы создать пустой кортеж, можно воспользоваться двумя конструкциями:
Давайте для примера создадим кортеж с 1 элементом:
Получилась строка. Но как же так? Давайте попробуем по-другому:
Оказывается все дело в запятой. Сами по себе скобки ничего не значат, точнее значат то, что внутри них находится одна инструкция, которая может быть отделена пробелами, переносом строк и т.д.
Можно подумать что у кортежей одни только плюсы, но это не так. Количество операций, которые можно применять над кортежами меньше, чем у списков. Это опять же связано с тем, что они неизменяемые. Получается что над кортежами можно применять все операции над списками, которые не изменяют список: сложение, умножение на число, методы index()
, count()
и некоторые другие операции.
Циклы¶
До сих пор в примерах хоть и обращались к разным объектам, добавляли и меняли их, всё ещё не было рассмотрено взаимодействие сразу с несколькими. Попробуем посчитать, сколько студентов получили оценку от 4 и выше. Для этого интуитивно кажется что нужно пройтись по всем оценкам от первой до последней, сравнить каждую с четверкой. Для прохода по списку, или итерации, используются циклы. Общий синтаксис таков:
Здесь example_list
– это некоторый итерируемый объект. Помимо списка в Python существуют и другие итерируемые объекты, но пока будем говорить о массивах.
Этот цикл работает так: указанной переменной item
присваивается первое значение из списка, и выполняется блок кода внутри цикла (этот блок, напомним, определяется отступом. Он выполняется весь от начала отступа и до конца, как и было объяснено в пятой лекции). Этот блок ещё иногда называют телом цикла. Потом переменной item
присваивается следующее значение (второе), и так далее. Переменную, кстати, можно называть как угодно, необязательно item
.
Итерацией называется каждый отдельный проход по телу цикла. Цикл всегда повторяет команды из тела цикла несколько раз. Два примера кода ниже аналогичны:
Понятно, что первый кусок кода обобщается на любой случай – хоть оценок десять, хоть тысяча. Второе решение не масштабируется, появляется много одинакового кода, в котором легко ошибиться (не поменять индекс, к примеру).
Движемся дальше. Так как каждый элемент списка закреплен за конкретным индексом, то в практике часто возникают задачи, логика которых завязана на индексах. Это привело к тому, что появилась альтернатива для итерации по списку. Функция range
принимает аргументы, аналогичные срезу в списке, и возвращает итерируемый объект, в котором содержатся целые числа (индексы). Так как аргументы являются аргументами функции, а не среза, то они соединяются запятой (как print(a, b)
нескольких объектов). Если подан всего один аргумент, то нижняя граница приравнивается к нулю. Посмотрим на практике, как сохранить номера (индексы) всех хорошо учащихся студентов:
В примере student_index
принимает последовательно все значения от 0
до 7
включительно. len(math_journal)
равняется 8
, а значит, восьмёрка сама не будет включена в набор индексов для перебора. На каждой итерации curent_student_grade
меняет своё значение, после чего происходит проверка. Если бы была необходимость пробежаться только по студентам, начиная с третьего, то нужно было бы указать range(2, len(math_journal))
(двойка вместо тройки потому, что индексация с нуля, ведь мы перебираем индексы массива).
Давайте немного модифицируем предыдущий пример:
В данном случае итерации происходят по объекту enumerate(math_journal)
. Метод enumerate()
возвращает итератор состоящий из tuple
, где каждый tuple
содержит пару состоящую из индекса элемента и самого элемента. В отличии от предыдущего примера, здесь не нужно получать оценку студента по его индексу (строчка номер 5), а можно напрямую ее использовать из цикла for
. Если предполагается работать одновременно и с индексами списка, и с его элементами, то метод enumerate()
– отличный способ сделать код чище!
Выше описаны основные концепции обращения со списками. Их крайне важно понять и хорошо усвоить, без этого писать любой код будет безумно сложно. Скопируйте примеры к себе в тетрадку, поиграйтесь, поменяйте параметры цикла и проанализируйте изменения.
List comprehensions¶
Некоторые циклы настолько просты, что занимают 2 или 3 строчки. Помимо прочего ещё и быстрее исполняются. Как пример – привести список чисел к списку строк:
Две части кода идентичны за вычетом того, что нижняя – с непонятной конструкцией в скобках – короче. Python позволяет в рамках одной строки произвести какие-либо простые преобразования (помним, что str()
– это вызов функции!). Фактически самый частый пример использования – это паттерн «применение функции к каждому объекту списка».
Что узнали из лекции¶
list
– это объект-контейнер, который хранит другие объекты разных типов; запись происходит упорядочено и последовательно, а каждому объекту присвоен целочисленный номер, начиная с нуля;- для добавления одного объекта в
list
нужно использовать метод объектаlist
–append()
, а для расширения списка сразу на несколько позиций пригодитсяextend()
; - проверить, входит ли объект в список, можно с помощью конструкции
obj in some_list
; - индексы могут быть отрицательными:
-1
,-2
... В таком случае нумерация начинается от последнего объекта; - можно получить часть списка, сделав срез с помощью конструкции
list[start_index : end_index]
, при этом объект на позицииend_idnex
не будет включён в возвращаемый список (т.е. срез работает не включительно по правую границу); - В зависимости от задачи – иногда удобнее и правильнее использовать
tuple
вместоlist
; - часто со списками используют циклы, которые позволяют итерироваться по объектам массива и выполнять произвольную логику в рамках отделенного отступом блока кода;
- для итерации по индексам можно использовать
range()
, а для итерации по элементам с индексами –enumerate()
; - простые циклы можно свернуть в list comprehension, и самый частый паттерн для такого преобразования – это применение некоторой функции к каждому объекту списка (если
x
это функция, то синтаксис будет таков:[x(item) for item in list])
).