Товары на сайте, предметы в игровом инвертаре или входящие звонки на сервисе... Как их можно объединить одним словом?
Мы вновь возвращаемся к типам данных в этой главе.
Ранее вы уже узнали, что существует 4 разных типов данных: строки, числа (целые и с плавающей точкой), булевые типы True/False.
Однако большинство программ работает не с отдельными переменными, а с набором данных. Например, будь то excel-таблица, или группа файлов, или вовсе вы хотите считывать подряд информацию с клавиатуры.
В предыдущей главе про циклы вы столкнулись с задачами на обработку последовательности, вычисляя, например, количество элементов последовательности, или максимум.
В то же время, во многих задачах нужно уметь сохранять всю последовательность, чтобы иметь возможность, к примеру, отсортировать ее или использовать повторно.
Вот, к примеру, задача про многоугольник. Если вы ее решили, то заметили, что каждую итерацию вы как бы забывали предыдущие значения точек и обновляли их значения через замену переменных.
Но что, если мы бы захотели нарисовать такой многоугольник на экране? Нам нужно запомнить эти точки, но как?
Или в случае с максимумом последовательности, если будет задача отсортировать эту последовательность - то как это сделать?
Прежде, чем объяснить, как работают списки в python, давайте откатимся немного назад и поговорим про то, а как же все таки хранятся данные в программах python. Понимание этих процессов поможет вам эффективно использовать ресурсы, применять эффективные операции, избегать ошибок с изменяемыми и неизменяемыми объектами (это будем подробно разбирать далее) или утечкой памяти и многое другое.
Ну и в целом понимание того, как устроена память и хранение данных в ЯП – это не бесполезное знание. Если в будущем вы решите перейти на более классические ЯП вроде C, вам будет легче понимать устройство именно C, ведь многие вещи в python опущены и реализованы проще, что иногда вгоняет в ступор.
Помните ли вы эту картинку из главы про ввод и вывод? Там на этом примере объяснялось, что в python при создании переменной не указывается место в памяти (как, например, в случае с языками вроде C/C++), а переменная является ссылкой на место в памяти. Такие ссылки называют еще и указателями.
То есть фактически, когда вы создаете переменную, вы не создаете сам объект (как опять же в том же C/C++), а делаете лишь ссылку на этот объект. Python самостоятельно определяет место в памяти, тип данных и объем, который должна занимать ячейка.
Давайте рассмотрим процесс на задаче из предыдущей главы, где нужно было посчитать перметр полигона (если вы не решали еще эту задачу, то вернитесь, решите ее, а потом продолжайте изучать эту главу).
Там пользователь вводил сначала значение для x1. Допустим, мы назначим сразу это значение и присвоим x1 число 2.
Что происходит в python? Где-то в памяти создается объект, в котором будет храниться значение. После этого x1 получает ссылку на этот объект.
x1 = 5
– Не совсем понятно, что происходит, когда я создаю переменную.
– Представь, что ты приходишь в камеру хранения и сдаешь туда свою сумку. Сумка — это твои данные, которые ты хочешь сохранить.
– А что тогда делает Python?
– Python берет твою сумку и кладет ее в ячейку. У каждой ячейки есть номер — это адрес в памяти. Затем он дает тебе бирку с номером этой ячейки.
– Значит, переменная — это бирка с номером ячейки, где хранятся мои данные?
– Да, именно так. Переменная указывает на место, где находятся твои данные. Если ты решишь изменить данные, Python просто поменяет бирку на другую ячейку с новыми данными.
Теперь мы хотим присвоить значение x1 к новой переменной, x2. Кажется, будто мы будем ссылаться на x1? Нет. X2 получит сразу ссылку на объект в памяти. По сути у нас будет две переменных с одинаковой ссылкой на объект в памяти.
x1 = 2 # Присвоили x1 значение 2
x2 = x1 # x2 получает ссылку ту же, что у x1
Для нас же проще думать, что x2 тоже теперь x1, но это сыграет свою роль дальше.
– Получается, что у одной ячейки хранения может быть сразу несколько ключей?
– Точно! Представь, что есть два ключа с одинаковым узором, которые открывают один и тот же шкафчик. Оба ключа (переменные) указывают на один и тот же объект (ячейку в памяти).
– А что случится, если я изменю значение одной из переменных?
– Хороший вопрос! Когда ты присваиваешь переменной новое значение, это как если бы тебе дали новый ключ, который указывает на другой шкафчик с новыми вещами. Старая ячейка при этом останется прежней, просто переменная больше не будет указывать на нее.
Догадались ли вы, что будет, если присвоить x1 новое значение? Ну, например, 3.
Если не догадались, объясняю: x1 просто получит новую ссылку на новый объект в памяти.
x1 = 2
x2 = x1
x1 = 3
print(x1, x2) # x2 по прежнему будет со значением 2
– Немного пример про шкафчик ломается. Все равно, что если бы мы узор на ключе переточили под новую ячейку?
– Да, вроде того. Но можно представить, что ключ электронный, и проблем с "переточкой" не будет :)
Список – это динамическая, упорядоченная последовательность ссылок на объекты в памяти, которая поддерживает произвольные типы данных и позволяет изменять свой размер (о как!). Звучит сложновато, но давайте разберем отдельно элементы этого термина:
Списки обозначаются квадратными скобками. Обращение к элементам списка также происходит через [] с указанием индекса элмента (порядкового номера, начиная с 0)
Динамическая последовательность
Списки могут изменять свой размер: добавлять и убирать элементы
Упорядоченная последовательность (или еще можно сказать, пронумерованная)
Элементы списка имеют определенный порядок и каждый элемент можно получить по его номеру в этом списке (нумерация при этом начинается с 0!)
Коллекция ссылок
Это означает, что в списке хранятся не сами данные, а ссылки на разные объекты в памяти, что позволяет хранить в списке данные любых типов.
– Значит, списки в Python — это как отдельные ячейки для хранения данных, как шкафчики для ключей в спортзале?
– Точно! Каждый элемент списка указывает на определенный объект, а сам список может расширяться или сжиматься, если нужно добавить или удалить элементы.
– А что делать, если места в списке не хватит?
– Не беспокойся, Python сам позаботится о том, чтобы выделить больше памяти, если это потребуется. Ты можешь просто добавлять элементы, и язык автоматически расширит список при необходимости.
Внимательно посмотрите, как это выглядит в памяти. Повторюсь, список в python – это набор ссылок, а не объектов (как, опять же, в некоторых языках) И так как это ссылки, вы можете "хранить" любые данные внутри списка.
Мы еще неоднократно вернемся к примерам хранение разных данных и в этой главе, и в течение остальных глав.
А сейчас, давайте перейдем к тому, как использовать списки и взаимодействовать с ними.
Теперь, когда мы разобрались с основами работы со списками, давайте рассмотрим методы их обработки и изменения.
Есть множество вариантов того, как вы можете взаимодействовать со списком и его элементами. Вы можете создавать список, добавлять, изменять и удалять его элементы, вызывать каждый элемент по отдельности или группами и многое другое. Какие-то методы взаимодействия со списками мы рассмотрим в этой главе сразу, другие появятся позже, в других главах.
Первый способ взаимодействия со списками (а также любыми другими объектами, в которых присутствует индексация, но об этом в другой раз) – это срезы.
Срезы представляют собой индексированный выбор одного или группы элементов элементов из списка. Вы уже видели этот выбор в записи data[0], это был срез по 1 элементу.
Обратите внимание снова, что адресация элементов списка начинается с нуля. Так что список из примера (где 7 элементов) не будет ничего содержать по адресу data[7], только ячейки от 0 до 6. Легко запутаться, если не держать это в голове.
Выбор 1 элемента из списка
Индексация может использоваться как с начала, в прямом порядке,
так и наоборот, с конца. Например, можно выбрать последний элемент.
a = [1, 1, 2, 3, 5, 8, 13, 21]
print(a[2], a[-6]) # 2 2
print(a[-1], a[7]) # 21 21
print(a[0], a[-2]) # 1 13
Как выглядит обращение к отрицательным индексам в коде
Если вашей целью стоит перебрать каждый элемент списка, то это можно очень хорошо сделать через цикл.
Идеальнее всего для этого подходят циклы for, но мы их рассмотрим подробно в следующей главе.
В целом, классическим способом обхода цикла является обход через переменную индекса. Чтобы использовать эту переменную, нужно с помощью программы считать от нуля до длины списка (не включитально).
Как вы уже поняли можно сделать срез на группу элементов. Делается это по следующему виду:
a[start : end : step]
Где start - это начало среза, end - окончание (не включительно), а step - шаг, с которым будут выбраны элементы.
При этом можно не указывать начало и/или окончание, в этом случае срез будет сделан с начала и/или до конца включительно.
Обратили ли вы внимание на использование отрицательных индексов в Python? Например, a[-1] возвращает последний элемент списка. В некоторых других языках программирования такая запись может не работать, так как это особенность синтаксиса Python. По сути, выражение a[-1] эквивалентно записи a[len(a) - 1], где мы вычитаем 1 из длины списка, чтобы получить индекс последнего элемента.
Отрицательный шаг в срезах (step) работает аналогично, только в обратном направлении. Например, запись a[start:end:-1] означает, что мы движемся от start к end в обратном порядке. Важно помнить, что если start больше end и шаг положительный, то срез не вернет элементов, так как направление движения не соответствует шагу.
A = [1, 2, 3, 4, 3, 3, 7]
# в JavaScript нет записи с -1, нужно писать полностью
# console.log(A[A.length - 1])
# в python можно написать двумя способами
print(A[len(A) - 1])
# и
print(A[-1])
# обозначают одно и то же. Такие упрощения называют синтаксическим сахаром, если помните
# запись
print(A[5:1:1])
# не будет работать, потому что из 5 нельзя получить 1 путем прибавления 1 (5+1=6)
print(A[5:1:-1])
# поэтому устанавливаем отрицательный индекс
Создайте программу "list-exercise.py".
Создайте список с 10 случайными числами (придумайте их сами).
Сначала выведите третий элемент этого списка.
Во второй строке выведите предпоследний элемент этого списка.
В третьей строке выведите первые пять элементов этого списка.
В четвертой строке выведите весь список, кроме последних двух символов.
В пятой строке выведите все элементы с четными индексами (считая, что индексация начинается с 0, поэтому элементы выводятся начиная с первого элемента по стандартному счету).
В шестой строке выведите все элементы с нечетными индексами, то есть начиная со второго элемента списка.
В седьмой строке выведите все элементы в обратном порядке.
В восьмой строке выведите все элемент этого списка через один в обратном порядке, начиная с последнего.
Взаимодействие со списками не ограничивается лишь обращением по индексу и получением данных. Одной из ключевых особенностей списков является то, что их элементы можно изменять.
Вы можете изменять элемент по индексу, добавлять новые элементы или удалять уже существующие.
Обратите внимание на код справа(или снизу). В нем используется метод append и pop, который применяется к списку. Вы уже сталкивались с ними, когда проходили главу про модули.
Важно понимать, что технически мы изменяем не сами элементы, а ссылки на них. Python работает с объектами в памяти, и когда элементы больше не используются, их ссылки могут быть изменены или удалены. Если объекты больше не нужны (на них нет ссылок), Python автоматически удаляет их из памяти с помощью специального механизма — сборщика мусора (garbage collector).
table = ['нож', 'вилка']
# Создаем список с двумя элементами
table[0] = 'ложка'
# Изменяем элемент по индексу. Теперь первый элемент списка — 'ложка', а не 'нож'
table.append('Ложка')
# Добавляем элемент 'Ложка' в конец списка
# Элементы всегда добавляются в конец списка
del table[1]
# Удаляем элемент по индексу 1, то есть 'вилка'
a = 5
del a
# Унарный оператор del в принципе может удалять любые ссылки на объекты, не только в списках.
knife = table.pop(0)
# Удаляем элемент по индексу 0, то есть 'Нож', и сохраняем его в переменную knife
# Отличие от del: метод pop() возвращает удаленный элемент
– Почему используется метод, а не просто функция?
– Методы — это функции, которые связаны с определенным объектом и выполняет определенные действия над этим объектом или его атрибутами. Например, у строк есть свои методы, такие как .upper() для преобразования в верхний регистр. У списков — свои методы, такие как .append() для добавления элементов. Обрати также внимание на ситаксис: методы вызываются с помощью точки ( . ) после объекта, к которому они применяются. Это такой удобный способ манипулировать данными в рабках определенного типа (строк, списков, словарей и т.д.). По сути это те же функции, но они принадлежат конкретным объектам.
– А почему есть append, который вызывается сам по себе, и есть pop, который вызывается в переменной?
– Потому что некоторые методы, такие как append, могут только изменять текущий объект. А такие методы, как pop, не только изменяют исходный объект, но и дополнительно возвращают значение. Есть и такие методы, которые только возвращают данные, но не меняют исходный объект.
– А можно ли вызывать pop вне переменной?
– Да, можно.
– Так, стоп, а что такое объект?
– Объект — это такая структура, которая объединяет данные и методы, которые с ними работают. В Python почти все является объектом: строки, списки, числа. Сейчас важно просто запомнить, что если функция вызывается через точку от переменной, это метод, потому что он "принадлежит" этому объекту.
Создайте программу "list-manipulations.py" и реализайте программу поэтапно.
Импортируйте из библиотеки random функцию randint
Создайте пустой список test_list
Напишите цикл, который будет повторяться 10 раз
Наполните список test_list десятью случайными элементами в диапазоне от -100 до 100
После завершения цикла
удалите 2 и 8 элементы (по индексу)
5 элемент (по счету) удалите, а его значение запишите в pop_elem
Если 3 элемент (по индексу) больше нуля, замените его значение на 'More', если меньше, то на 'Less'. Если значение элемента равно нулю, оставьте его без изменений.
Часто возникает необходимость не вводить элементы списка по одному (например, через цикл), а сразу преобразовать строку, разделенную определенным символом, в список. Для этого используется метод split. В обратном направлении работает метод строки join, который объединяет элементы списка в одну строку.
Важно: после того как метод split преобразует строку в список, все элементы этого списка останутся строкового типа str.
И наоборот, для того чтобы объединить элементы списка в строку с помощью join, все элементы должны быть типа str.
table = 'нож вилка'
# Строка с двумя словами
table_list = table.split()
# Преобразуем строку в список, используя метод строки split(), который разделяет строку по пробелам
# Теперь table_list = ['нож', 'вилка']
print(table_list)
table = 'нож, вилка'
# Меняем разделитель
table_list = table.split(', ')
table = input('Какие предметы лежат на столе? Перечислите через пробел')
# Тут пользователь вводит сам текст
table_list = table.split(' ')
books_list = ['Булгаков', 'Пушкин']
# Создаем список с именами авторов
print(', '.join(books_list))
# Используем метод строки join() для объединения элементов списка в строку с разделителем ", "
# Вывод: Булгаков, Пушкин
Создайте программу "list-manipulations2.py" и реализайте программу поэтапно.
Пусть пользователь вводит 1 строку с 10 разными числами через пробел
Преобраразуйте эту строку в список
Создайте еще одну переменную с пустым списком
По циклу обработайте первый список. Выберите из него все числа, которые будут кратны 2 и добавьте их во второй список. Помните, что у вас всегда 10 элементов.
Выведите второй список в виде строки с разделителем " / "
Естественно, это не все операции со списком. Справа я выпишу некоторые другие популярные функции и методы работы со списками.
A = [1, 2, 3, 4, 3, 3, 7]
A.extend([8,9])
# Добавляет все элементы из переданного итератора (например, другого списка) в конец текущего списка.
A.insert(1, 1.5)
# Вставляет элемент на указанную позицию в списке. Все элементы, начиная с этого индекса, смещаются вправо.
A.reverse()
# Изменяет порядок элементов списка на обратный
A.sort()
# Сортирует список. В качестве ключа можно указать функцию сортировки (будет далее в главе про функции)
A = [1, 2, 3, 4, 3, 3, 7]
print(len(A))
# Вывести длину списка
print(max(A), min(A))
# Вывести максимальное и минимальное значение из А
print(2 in A, 9 not in A)
# Проверить, есть ли 2 в А, 9 не в А (то же самое, что и not(9 in A)
print(A.count(3))
# Проверить количество элементов 3 в А
print(A.index(7))
# Возвращает индекс элемента (первый слева) Можно указать начало и конец поиска: A.index(element, start=0, end=len(list))
Создайте программу "list-manipulations3.py" и реализайте программу поэтапно.
Пусть пользователь вводит 1 строку с неизвестным количеством чисел через пробел. Преобразуйте эту строку в список.
Напишите цикл, который будет повторяться столько раз, сколько элементов в списке
Выведите на экран все элементы, что больше 0
Создайте программу "list-manipulations4.py" и реализайте программу поэтапно.
Пусть пользователь вводит 1 строку с неизвестным количеством чисел через пробел. Преобразуйте эту строку в список.
Напишите цикл, который будет повторяться столько раз, сколько элементов в списке
Выведите на экран все элементы, которые будут четные по индексу (0, 2, 4, 6 и тд)
Взаимодействие со списками не ограничивается лишь изменением их элементов. Важно помнить, что при создании копий списков можно столкнуться с неожиданными результатами.
Если вы присваиваете одну переменную другой (например, a = b), обе переменные будут ссылаться на один и тот же объект в памяти.
Изменения, внесенные через одну из них, отразятся на другой. Чтобы создать независимую копию списка, нужно использовать специальные методы.
Важно понимать, что операция a=b не создает новый список. Это просто присваивание ссылки на уже существующий объект. Поэтому, если вы измените объект через переменную a, то изменения будут видны и через переменную b, так как они обе ссылаются на один и тот же объект.
На картинке справа нарисован пример кода:
data = [1, 'one', True]
data2 = data
data[1] = 'two'
Проверьте самостоятельно, чему равен data2
Вы можете создать полную копию списка с помощью срезов или метода data2 = data.copy() или полного среза, типа data2 = data[:]. Это важно, когда необходимо сохранить исходный список неизменным.
Важно понимать, что при поверхностном копировании (copy()) копируются только ссылки на элементы списка. Если элементы списка сами являются изменяемыми объектами (например, другие списки), то изменения этих элементов отразятся в обеих копиях. Чтобы создать полностью независимую копию (глубокое копирование), нужно использовать функцию deepcopy из модуля copy.
– Что означает такая запись: [1, 2, [3, 4]]?
– Это список, внутри которого есть еще один список. Представь, что это как большая коробка, в которой лежит еще одна маленькая коробка с дополнительными вещами.
– То есть я могу хранить списки внутри других списков?
– Именно так! Такие списки называют вложенными. Их удобно использовать, когда нужно хранить данные, которые логически сгруппированы, например, таблицу или список задач с подзадачами. Однако работать с вложенными списками бывает сложнее, так как нужно помнить о том, как к ним обращаться.