2.5 Последовательности#
Первой группой типов данных, относящихся к коллекциям, мы назвали последовательности. Последовательности - это упорядоченные структуры данных, из которых можно получить элемент по индексу (что мы уже видели при работе со строками). К последовательностям можно отнести следующие типы данных:
Строка
str- текстовая последовательностьДиапазон
range- последовательность целых чисел в заданном диапазоне с заданным шагомСписок
list- изменяемая последовательность объектов любых типовКортеж
tuple- неизменяемая последовательность объектов любых типов
Остановимся на каждом подробнее.
Бинарные последовательности
Также можно было выделить блок бинарных последовательностей: bytes, bytearray, memoryview. Однако в рамках данных материалов они не рассматриваются.
Диапазоны#
Одна из наиболее простых последовательностей, которую мы часто будем встречать - диапазон. Это тип данных, представляющий из себя последовательность целых чисел и не обладающий каким-либо дополнительным функционалом. Основное его применение - ограничение количества итераций в счётных циклах, когда мы создаем последовательность из n целых чисел и потом выполняем n итераций по этой последовательности.
Создаётся диапазон с помощью вызова функции range, которой необходимо передать от одного до трех целочисленных параметров. Эти параметры определяют границы создаваемого диапазона и шаг между числами в нём:
При передаче одного параметра считается, что мы создаем последовательность от 0 включительно до указанного параметра не включительно с шагом 1
# Диапазон можно присвоить переменной и выполнять цикл уже по ней.
# Но чаще используется вариант, когда диапазон определяют сразу в цикле
for n in range(5): # цикл выполнит 5 итераций
print(n)
0
1
2
3
4
При передаче двух параметров считается, что мы создаем последовательность от первого параметра включительно до второго параметра не включительно с шагом 1.
# Помним, что все параметры при перечислении разделяются запятыми
for n in range(-2, 3):
print(n)
-2
-1
0
1
2
При передаче трёх параметров считается, что мы создаем последовательность от первого параметра включительно до второго параметра не включительно с шагом равным третьему параметру.
for n in range(-10, 11, 5):
print(n)
-10
-5
0
5
10
После определения диапазона с ним, как и с любой другой последовательностью, можно выполнить несколько действий.
Обратиться к элементу последовательности по индексу или получить её срез, как это уже делалось для строк
even_numbers = range(0, 100, 2)
print(even_numbers[5])
print(even_numbers[5:10])
10
range(10, 20, 2)
Получить длину последовательности, передав её как параметр в вызов функции
len
even_numbers = range(0, 100, 2)
len(even_numbers)
50
Проверить входит ли элемент в последовательность с помощью оператора
in(слева отinуказывается элемент, справа - последовательность, результат операции возвращает логический тип -True\False).
even_numbers = range(0, 100, 2)
10 in even_numbers
True
Кортежи#
Ещё одна простая последовательность, с которой мы часто будем работать, - это кортежи. Кортеж - неизменяемая последовательность, которая используется для хранения данных. Создается путем перечисления в круглых скобках через запятую элементов, которые в него должны войти:
number_tuple = (1, 2, 3, 4) # кортеж из чисел
str_sequence = ("1", "2", "3", "4") # кортеж из строк
mixed_sequence = (1, 2, "3", "4") # кортеж, состоящий из разных типов данных
В сравнении с диапазонами кортежи обладают дополнительным встроенным функционалом и позволяют найти индекс нужного элемента в кортеже и посчитать, сколько таких элементов в данный кортеж входит:
number_tuple = (1, 2, 2, 3, 3, 3, 4, 4)
number_tuple.index(4) # возвращает индекс первого вхождения элемента в кортеж
# Если пробовать найти элемент, отсутствующий в кортеже, выполнение кода прекратится с ошибкой
6
number_tuple.count(4) # посчитает количество вхождений элемента в кортеж
# Если попробовать посчитать вхождения элемента, отсутствующего в кортеже, получим 0
2
Списки#
Когда последовательность нужно не просто хранить, а также изменять и дополнять в процессе, вместо кортежей будут использоваться списки (списки ещё расширяют функционал кортежей). Создание списков похоже на создание кортежа с той разницей, что вместо круглых скобок используются квадратные:
number_sequence = [1, 2, 3, 4] # список из чисел
str_sequence = ["1", "2", "3", "4"] # список из строк
mixed_sequence = [1, 2, "3", "4"] # список, состоящий из разных типов данных
Для некоторых задач понадобится использовать ещё и пустые списки, которые будут заполняться в процессе. Создать пустой список можно двумя способами:
# 1. оставляем пустые скобки
empty_list = []
# 2. вызываем тип list
empty_list = list()
В плане функционала у списков есть множество методов, которые позволяют преобразовывать сам объект, в том числе добавляя в него новые элементы, убирая их или изменяя существующие.
Для добавления новых элементов в список обычно используются два метода: append и extend.
append- добавляет в конец списка один новый элемент. Всегда принимает только один параметр
my_list = []
my_list.append(1) # В качестве параметра указываем, какой элемент мы добавляем в последовательность
my_list.append([1, 2]) # Если попробовать добавить другой список, он также добавится как один элемент
print(my_list)
[1, [1, 2]]
extend- расширяет список, добавляя в конец списка элементы другой последовательности. В качестве параметра всегда принимает одну последовательность
my_list.extend([3, 4]) # 3 и 4 добавятся в список my_list как отдельные элементы
print(my_list)
[1, [1, 2], 3, 4]
Реже в случаях, когда элемент нужно добавить на конкретную позицию, используется метод insert. При вызове insert нужно всегда указывать два параметра: первый - по какому индексу будет вставлен элемент, второй - какой именно элемент будет вставлен.
my_list.insert(0, 5) # Вставит 5 на место с индексом 0, остальные элементы сдвинутся вправо
print(my_list)
[5, 1, [1, 2], 3, 4]
Для удаления элемента могут использоваться два метода: remove и pop
remove по аналогии с index будет удалять из списка первое вхождение элемента в последовательность
my_list.remove(5) # Если попробовать удалить элемент, которого нет, то будет ошибка
print(my_list)
[1, [1, 2], 3, 4]
pop позволяет удалить элемент по индексу. Помимо простого удаления pop еще и возвращает удаленный элемент. Так, элемент можно будет достать из списка и сохранить в отдельную переменную.
my_list.pop(0)
print(my_list)
[[1, 2], 3, 4]
deleted_element = my_list.pop(0)
print("deleted:", deleted_element)
print("left:", my_list)
deleted: [1, 2]
left: [3, 4]
Последнее, что нам обязательно понадобится при работе со списками (с любыми изменяемыми последовательностями), - это изменение существующих значений. Мы уже умеем делать обращение по индексу, чтобы получить нужный элемент. Также обращение по индексу можно использовать при операциях присваивания, для изменения значения в последовательности по конкретному индексу.
number_sequence = [1, 2, 3, 4]
number_sequence[0] = 10
print(number_sequence)
[10, 2, 3, 4]
Список или кортеж
Когда использовать кортеж:
Данные не должны изменяться (координаты точки, RGB-цвета, константы)
Функция возвращает несколько значений:
return x, yВажна защита от случайного изменения
Когда использовать список:
Данные будут изменяться (добавление, удаление, замена элементов)
Размер коллекции заранее неизвестен
Почему не стоит использовать всегда только списки:
Кортеж занимает меньше места в памяти (списки выделяют память с запасом и хранят больше информации о доступных методах).
Python может кэшировать неизменяемые кортежи небольших размеров
На практике разница в производительности и используемой памяти заметна только на больших объёмах данных. Главное преимущество кортежей - гарантия неизменности данных.
Изменяемость объектов#
Ссылки на объекты#
Мы уже знаем, что переменные в Python - это ссылки на объекты. Это значит, что при присваивании b = a переменная b не получает копию значения, а начинает ссылаться на тот же самый объект, что и a. Для неизменяемых типов данных (строк, чисел, кортежей) это не создаёт проблем - мы не можем изменить объект на месте, только создать новый. Но с изменяемыми типами, такими как списки, ситуация оказывается сложнее.
Когда мы присваиваем список другой переменной, обе переменные начинают ссылаться на один и тот же объект в памяти. Это можно проверить с помощью функции id(), которая возвращает идентификатор объекта:
a = [1, 2, 3]
b = a # b теперь ссылается на тот же объект, что и a
print("id(a):", id(a))
print("id(b):", id(b))
print("Это один объект?", a is b) # оператор is проверяет что переменные ссылаются на один id
id(a): 140145526449344
id(b): 140145526449344
Это один объект? True
Поскольку это один и тот же объект, изменение через одну переменную повлияет на другую:
b.append(4) # меняем через b
print("a:", a) # a тоже изменился!
print("b:", b)
a: [1, 2, 3, 4]
b: [1, 2, 3, 4]
Для неизменяемых типов такой проблемы нет - мы не можем изменить объект на месте, только создать новый:
x = "Moscow"
y = x # y ссылается на тот же объект
# Но при "изменении" создаётся новый объект:
y = y.upper() # создаётся новая строка, y начинает ссылаться на новый объект
print("x:", x) # x не изменился
print("y:", y) # y - новый объект
print("Это один объект?", x is y) # False - разные объекты
x: Moscow
y: MOSCOW
Это один объект? False
Практический пример: округление координат#
Представьте, что у нас есть список координат городов и мы хотим округлить значения для отображения:
# Исходные координаты городов
cities_coords = [55.7558, 37.6173, 59.9343, 30.3351]
# "Копируем" для обработки (на самом деле - та же ссылка)
rounded_coords = cities_coords
# Округляем координаты
for i in range(len(rounded_coords)):
rounded_coords[i] = round(rounded_coords[i], 1)
print("Исходные:", cities_coords) # Тоже изменились!
print("Округлённые:", rounded_coords)
Исходные: [55.8, 37.6, 59.9, 30.3]
Округлённые: [55.8, 37.6, 59.9, 30.3]
Исходные данные были потеряны - обе переменные ссылались на один список. Чтобы этого избежать, нужно создать независимую копию.
Поверхностное копирование#
Для списков есть несколько способов создать поверхностную копию - новый список с теми же элементами:
original = [55.7558, 37.6173, 59.9343, 30.3351]
# Способ 1: срез
copy1 = original[:]
# Способ 2: метод .copy()
copy2 = original.copy()
# Способ 3: вызов list()
copy3 = list(original)
# Проверяем, что это разные объекты
print(original is copy1) # False - разные объекты
print(original is copy2) # False - разные объекты
print(original is copy3) # False - разные объекты
False
False
False
Теперь при изменении копии оригинал остаётся нетронутым:
cities_coords = [55.7558, 37.6173, 59.9343, 30.3351]
rounded_coords = cities_coords[:] # создаём независимую копию
for i in range(len(rounded_coords)):
rounded_coords[i] = round(rounded_coords[i], 1)
print("Исходные:", cities_coords) # [55.7558, 37.6173, 59.9343, 30.3351]
print("Округлённые:", rounded_coords) # [55.8, 37.6, 59.9, 30.3]
Исходные: [55.7558, 37.6173, 59.9343, 30.3351]
Округлённые: [55.8, 37.6, 59.9, 30.3]
Проблема вложенных списков#
Поверхностное копирование создаёт новый список, но элементы внутри него остаются теми же объектами. Для списка простых чисел это не проблема, но для вложенных списков - критично:
# Координаты обычно хранятся как список списков: [[широта, долгота], ...]
original = [[55.7558, 37.6173], [59.9343, 30.3351]]
copy = original[:] # поверхностная копия
print("Это разные списки:", original is copy) # True - разные списки
print("Но элементы - те же объекты:", original[0] is copy[0]) # True - те же!
Это разные списки: False
Но элементы - те же объекты: True
Внешний список - новый, но вложенные списки - те же самые. Поэтому изменение вложенного списка в копии затронет и оригинал:
original = [[55.7558, 37.6173], [59.9343, 30.3351]]
copy = original[:]
# Округляем координаты во вложенных списках
for coord in copy:
coord[0] = round(coord[0], 1)
coord[1] = round(coord[1], 1)
print("Оригинал:", original) # Тоже изменился!
print("Копия:", copy)
Оригинал: [[55.8, 37.6], [59.9, 30.3]]
Копия: [[55.8, 37.6], [59.9, 30.3]]
Глубокое копирование#
Чтобы создать полностью независимую копию, включая все вложенные объекты, нужно использовать глубокое копирование из модуля copy:
# В Python есть функционал, который нужно импортировать для использования
# Подробнее про импорт и часто используемые встроенные пакеты и модули будет рассказано позже
import copy
original = [[55.7558, 37.6173], [59.9343, 30.3351]]
deep_copy = copy.deepcopy(original) # глубокое копирование
# Теперь даже вложенные списки - разные объекты
print("Это разные списки?", original is deep_copy) # True
print("Вложенные тоже разные?", original[0] is deep_copy[0]) # True
Это разные списки? False
Вложенные тоже разные? False
Теперь изменение копии никак не затронет оригинал:
original = [[55.7558, 37.6173], [59.9343, 30.3351]]
deep_copy = copy.deepcopy(original)
for coord in deep_copy:
coord[0] = round(coord[0], 1)
coord[1] = round(coord[1], 1)
print("Оригинал:", original) # Не изменился
print("Копия:", deep_copy) # Изменённая
Оригинал: [[55.7558, 37.6173], [59.9343, 30.3351]]
Копия: [[55.8, 37.6], [59.9, 30.3]]
Копирование списков
Поверхностная копия ([:], .copy(), list()) - создаёт новый список, но элементы внутри - те же объекты. Подходит для списков простых значений (числа, строки).
Глубокая копия (copy.deepcopy()) - создаёт новый список и рекурсивно копирует все вложенные объекты. Необходима для вложенных структур.
Методы списков, кортежей, диапазонов
С полным перечнем методов списков и примерами их использования можно ознакомиться в справочном материале.
Методы множеств
С полным перечнем методов множеств и примерами их использования можно ознакомиться в справочном материале.