2.5 Последовательности#

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

  1. Строка str - текстовая последовательность

  2. Диапазон range - последовательность целых чисел в заданном диапазоне с заданным шагом

  3. Список list - изменяемая последовательность объектов любых типов

  4. Кортеж tuple - неизменяемая последовательность объектов любых типов

Остановимся на каждом подробнее.

Диапазоны#

Одна из наиболее простых последовательностей, которую мы часто будем встречать - диапазон. Это тип данных, представляющий из себя последовательность целых чисел и не обладающий каким-либо дополнительным функционалом. Основное его применение - ограничение количества итераций в счётных циклах, когда мы создаем последовательность из 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

После определения диапазона с ним, как и с любой другой последовательностью, можно выполнить несколько действий.

  1. Обратиться к элементу последовательности по индексу или получить её срез, как это уже делалось для строк

even_numbers = range(0, 100, 2)

print(even_numbers[5])
print(even_numbers[5:10])
10
range(10, 20, 2)
  1. Получить длину последовательности, передав её как параметр в вызов функции len

even_numbers = range(0, 100, 2)

len(even_numbers)
50
  1. Проверить входит ли элемент в последовательность с помощью оператора 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]

Изменяемость объектов#

Ссылки на объекты#

Мы уже знаем, что переменные в 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()) - создаёт новый список и рекурсивно копирует все вложенные объекты. Необходима для вложенных структур.


Методы списков, кортежей, диапазонов

С полным перечнем методов списков и примерами их использования можно ознакомиться в справочном материале.

Методы множеств

С полным перечнем методов множеств и примерами их использования можно ознакомиться в справочном материале.