14. Списки: упорядоченные коллекции элементов
До сих пор в этой книге мы работали с отдельными фрагментами данных: одиночными числами, строками и булевыми значениями. Но реальным программам часто нужно работать с коллекциями связанных элементов — списком имён студентов, серией измерений температуры, коллекцией цен продуктов или последовательностью команд пользователя. Python список(list) — это базовый инструмент для хранения и работы с упорядоченными коллекциями данных.
Список — это последовательность(sequence), которая может хранить несколько элементов в определённом порядке. В отличие от строк (которые могут содержать только символы), списки могут содержать данные любого типа: числа, строки, булевы значения или даже другие списки. Списки также изменяемые(mutable), то есть вы можете менять их содержимое после создания — добавлять элементы, удалять элементы или изменять существующие.
В этой главе мы рассмотрим, как создавать списки, получать доступ к их элементам, изменять их и использовать для решения практических задач программирования. К концу вы поймёте, почему списки — одна из самых мощных и часто используемых структур данных Python.
14.1) Создание списков и доступ к элементам
14.1.1) Создание списков с помощью квадратных скобок
Самый распространённый способ создать список — заключить элементы в квадратные скобки [], разделив элементы запятыми. Вот простой пример:
# Список имён студентов
students = ["Alice", "Bob", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']Обратите внимание, как Python отображает список: он показывает квадратные скобки и ставит кавычки вокруг каждой строки. Это представление(representation) списка — то, как Python показывает, что находится внутри.
Списки могут содержать данные любого типа. Вот список результатов теста:
# Список целочисленных результатов
scores = [85, 92, 78, 95, 88]
print(scores) # Output: [85, 92, 78, 95, 88]Вы даже можете смешивать разные типы в одном списке, хотя на практике это встречается реже:
# Список смешанных типов (встречается реже, но допустимо)
mixed_data = ["Alice", 25, True, 3.14]
print(mixed_data) # Output: ['Alice', 25, True, 3.14]Пустой список(empty list) не содержит элементов и создаётся просто с помощью квадратных скобок:
# Пустой список
empty = []
print(empty) # Output: []
print(len(empty)) # Output: 0Функция len(), которую мы использовали со строками, также работает со списками — она возвращает количество элементов в списке.
14.1.2) Понимание порядка списка и позиций
Списки сохраняют порядок(order), в котором вы добавляете элементы. Первый добавленный элемент остаётся первым, второй остаётся вторым и так далее. Этот порядок критически важен, потому что позволяет получать доступ к конкретным элементам по их позиции(position) (также называемой индексом(index)).
Python использует индексацию с нуля(zero-based indexing): первый элемент находится в позиции 0, второй — в позиции 1 и так далее. Сначала это может показаться необычным, но это соглашение используется во многих языках программирования.
Посмотрим, как это работает на практике:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Доступ к первому студенту (индекс 0)
first_student = students[0]
print(first_student) # Output: Alice
# Доступ к третьему студенту (индекс 2)
third_student = students[2]
print(third_student) # Output: CharlieОбратите внимание: чтобы получить третьего студента, мы используем индекс 2, а не 3. Это потому, что отсчёт начинается с 0.
14.1.3) Доступ к элементам с положительными индексами
Чтобы получить доступ к элементу списка, напишите имя списка, а затем индекс в квадратных скобках: list_name[index]. Индекс должен быть целым числом в допустимом диапазоне (от 0 до len(list) - 1).
Вот практический пример работы с ценами продуктов:
# Цены продуктов в долларах
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# Доступ к конкретным ценам
first_price = prices[0]
last_index = len(prices) - 1 # Вычисляем последний допустимый индекс
last_price = prices[last_index]
print(f"First product costs: ${first_price}") # Output: First product costs: $19.99
print(f"Last product costs: ${last_price}") # Output: Last product costs: $8.99Почему для последнего индекса мы используем len(prices) - 1? Потому что если в списке 5 элементов, индексы будут 0, 1, 2, 3, 4 — последний допустимый индекс всегда на единицу меньше длины.
Вы также можете использовать индексы в выражениях и вычислениях:
scores = [85, 92, 78, 95, 88]
# Вычисляем среднее значение первых трёх результатов
first_three_average = (scores[0] + scores[1] + scores[2]) / 3
print(f"Average of first three: {first_three_average}") # Output: Average of first three: 85.014.1.4) Отрицательные индексы: счёт с конца
Python предоставляет удобную возможность: отрицательные индексы(negative indices) позволяют получать доступ к элементам с конца списка. Индекс -1 указывает на последний элемент, -2 — на предпоследний и так далее.
students = ["Alice", "Bob", "Charlie", "Diana"]
# Доступ с конца
last_student = students[-1]
second_to_last = students[-2]
print(last_student) # Output: Diana
print(second_to_last) # Output: CharlieЭто особенно полезно, когда вам нужен последний элемент, но вы не хотите вычислять len(list) - 1:
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# Эти два подхода эквивалентны
last_price_method1 = prices[len(prices) - 1]
last_price_method2 = prices[-1]
print(last_price_method1) # Output: 8.99
print(last_price_method2) # Output: 8.99Вот как положительные и отрицательные индексы соответствуют одним и тем же элементам:
14.1.5) Что происходит при неверных индексах
Если вы попытаетесь обратиться к индексу, которого не существует, Python вызывает IndexError:
students = ["Alice", "Bob", "Charlie"]
# WARNING: В этом списке индексы 0, 1, 2 (или -3, -2, -1) — только для демонстрации
# Попытка обратиться к индексу 3 вызывает ошибку
# PROBLEM: Индекс 3 не существует в списке из 3 элементов
# print(students[3]) # IndexError: list index out of rangeЭта ошибка — способ Python сообщить вам, что вы запросили элемент, которого там нет.
14.2) Индексация и срезы списков
14.2.1) Понимание основ срезов списков
Так же как мы можем делать срезы строк (как мы изучили в главе 5), мы можем делать срезы(slice) списков, чтобы извлекать их части. Срез создаёт новый список, содержащий подмножество элементов исходного списка. Синтаксис: list[start:stop], где start — индекс, с которого начинается срез (включительно), а stop — где он заканчивается (исключительно).
numbers = [10, 20, 30, 40, 50, 60, 70]
# Получить элементы от индекса 1 до (но не включая) индекс 4
subset = numbers[1:4]
print(subset) # Output: [20, 30, 40]Срез [1:4] включает индексы 1, 2 и 3, но останавливается перед индексом 4. Это правило «stop не включается» такое же, как и при срезах строк.
Рассмотрим практический пример с именами студентов:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# Получить первых трёх студентов
first_three = students[0:3]
print(first_three) # Output: ['Alice', 'Bob', 'Charlie']
# Получить студентов с индекса 2 по 4
middle_group = students[2:5]
print(middle_group) # Output: ['Charlie', 'Diana', 'Eve']14.2.2) Пропуск start или stop в срезах
Вы можете опустить индекс start, чтобы сделать срез от начала, или опустить индекс stop, чтобы сделать срез до конца:
scores = [85, 92, 78, 95, 88, 91, 87]
# От начала до индекса 3
first_few = scores[:3]
print(first_few) # Output: [85, 92, 78]
# От индекса 4 до конца
last_few = scores[4:]
print(last_few) # Output: [88, 91, 87]
# Весь список (от начала до конца)
all_scores = scores[:]
print(all_scores) # Output: [85, 92, 78, 95, 88, 91, 87]Срез [:] создаёт копию(copy) всего списка. Это полезно, когда вы хотите работать с дубликатом, не изменяя оригинал — мы рассмотрим это подробнее в разделе 14.6.
14.2.3) Использование отрицательных индексов в срезах
Отрицательные индексы работают в срезах так же, как и при доступе к одному элементу. Это особенно полезно, чтобы получать элементы с конца:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# Получить последних трёх студентов
last_three = students[-3:]
print(last_three) # Output: ['Diana', 'Eve', 'Frank']
# Получить всех, кроме последних двух студентов
all_but_last_two = students[:-2]
print(all_but_last_two) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']
# Получить от третьего с конца до второго с конца
middle_from_end = students[-3:-1]
print(middle_from_end) # Output: ['Diana', 'Eve']14.2.4) Срезы с шагом
Вы можете добавить третий параметр, чтобы управлять шагом(step) (сколько индексов пропускать между элементами). Полный синтаксис: list[start:stop:step]:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Каждый второй номер, начиная с индекса 0
evens = numbers[0:10:2]
print(evens) # Output: [0, 2, 4, 6, 8]
# Каждый третий номер, начиная с индекса 1
every_third = numbers[1:10:3]
print(every_third) # Output: [1, 4, 7]Вы также можете использовать отрицательный шаг(negative step), чтобы развернуть список:
numbers = [1, 2, 3, 4, 5]
# Развернуть список
reversed_numbers = numbers[::-1]
print(reversed_numbers) # Output: [5, 4, 3, 2, 1]Срез [::-1] означает «начать с конца, идти к началу, двигаясь назад с шагом 1». Это распространённый идиоматический приём Python для разворота последовательностей.
14.2.5) Срезы никогда не вызывают IndexError
В отличие от доступа к одному элементу, срезы очень «прощают» ошибки. Если вы укажете индексы вне диапазона списка, Python просто подстроит их под границы:
numbers = [10, 20, 30, 40, 50]
# Запрашиваем больше, чем существует
extended_slice = numbers[2:100]
print(extended_slice) # Output: [30, 40, 50]
# Начинаем за пределами конца
empty_slice = numbers[10:20]
print(empty_slice) # Output: []Такое поведение полезно, потому что означает, что вам не нужно беспокоиться о точных границах при срезах — Python аккуратно обрабатывает крайние случаи.
14.3) Изменение списков и распространённые методы списков
14.3.1) Списки изменяемы: изменение элементов
В отличие от строк, которые неизменяемы, списки изменяемы(lists are mutable) — вы можете менять их содержимое после создания. Вы можете изменять отдельные элементы, присваивая новые значения по конкретным индексам:
# Начинаем со списка цен
prices = [19.99, 24.50, 15.75, 32.00]
print(prices) # Output: [19.99, 24.5, 15.75, 32.0]
# Обновляем вторую цену (индекс 1)
prices[1] = 22.99
print(prices) # Output: [19.99, 22.99, 15.75, 32.0]
# Обновляем последнюю цену, используя отрицательную индексацию
prices[-1] = 29.99
print(prices) # Output: [19.99, 22.99, 15.75, 29.99]Эта изменяемость — мощная возможность: она означает, что вы можете обновлять данные «на месте», не создавая новые списки. Однако это также означает, что нужно быть внимательным к непреднамеренным изменениям — мы обсудим это в разделе 14.6.
14.3.2) Добавление элементов с append()
Метод append() добавляет один элемент в конец списка. Это одна из самых часто используемых операций со списками:
# Начинаем с пустой корзины покупок
cart = []
print(cart) # Output: []
# Добавляем элементы по одному
cart.append("Milk")
print(cart) # Output: ['Milk']
cart.append("Bread")
print(cart) # Output: ['Milk', 'Bread']
cart.append("Eggs")
print(cart) # Output: ['Milk', 'Bread', 'Eggs']Обратите внимание, что append() изменяет список на месте(in place) — он не возвращает новый список. Метод возвращает None, поэтому результат не нужно присваивать:
scores = [85, 92, 78]
result = scores.append(95)
print(scores) # Output: [85, 92, 78, 95]
print(result) # Output: None14.3.3) Вставка элементов в конкретные позиции с insert()
Если append() всегда добавляет в конец, то insert() позволяет добавить элемент в любую позицию. Синтаксис: list.insert(index, item):
students = ["Alice", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Charlie', 'Diana']
# Вставляем "Bob" по индексу 1 (между Alice и Charlie)
students.insert(1, "Bob")
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']Когда вы вставляете по индексу, элемент, который был в этой позиции (и все элементы после него), сдвигаются вправо:
numbers = [10, 20, 30, 40]
print(numbers) # Output: [10, 20, 30, 40]
# Вставляем 25 по индексу 2
numbers.insert(2, 25)
print(numbers) # Output: [10, 20, 25, 30, 40]Вы можете вставлять в начало, используя индекс 0:
priorities = ["Medium", "Low"]
priorities.insert(0, "High")
print(priorities) # Output: ['High', 'Medium', 'Low']Если вы укажете индекс больше длины списка, insert() просто добавит элемент в конец (как append()):
items = [1, 2, 3]
items.insert(100, 4)
print(items) # Output: [1, 2, 3, 4]14.3.4) Удаление элементов с remove()
Метод remove() удаляет первое вхождение(first occurrence) указанного значения из списка:
fruits = ["apple", "banana", "cherry", "banana", "date"]
print(fruits) # Output: ['apple', 'banana', 'cherry', 'banana', 'date']
# Удаляем первый "banana"
fruits.remove("banana")
print(fruits) # Output: ['apple', 'cherry', 'banana', 'date']Обратите внимание: был удалён только первый "banana" — второй остался. Если попытаться удалить значение, которого нет, Python вызовет ValueError:
numbers = [10, 20, 30]
# WARNING: Попытка удалить несуществующее значение — только для демонстрации
# PROBLEM: 40 нет в списке
# numbers.remove(40) # ValueError: list.remove(x): x not in listЧтобы избежать этой ошибки, можно проверить, существует ли элемент, прежде чем удалять его:
cart = ["Milk", "Bread", "Eggs"]
item_to_remove = "Butter"
if item_to_remove in cart:
cart.remove(item_to_remove)
print(f"Removed {item_to_remove}")
else:
print(f"{item_to_remove} not in cart")
# Output: Butter not in cart14.3.5) Удаление и возврат элементов с pop()
Метод pop() удаляет элемент по указанному индексу и возвращает(returns) его. Если индекс не указан, он удаляет и возвращает последний элемент:
scores = [85, 92, 78, 95, 88]
# Удаляем и получаем последний результат
last_score = scores.pop()
print(f"Removed: {last_score}") # Output: Removed: 88
print(scores) # Output: [85, 92, 78, 95]
# Удаляем и получаем результат по индексу 1
second_score = scores.pop(1)
print(f"Removed: {second_score}") # Output: Removed: 92
print(scores) # Output: [85, 78, 95]Это полезно, когда нужно обрабатывать элементы списка по одному:
tasks = ["Write code", "Test code", "Deploy code"]
while len(tasks) > 0:
current_task = tasks.pop(0) # Удаляем из начала
print(f"Working on: {current_task}")
# Output:
# Working on: Write code
# Working on: Test code
# Working on: Deploy code
print(tasks) # Output: []14.3.6) Расширение списков с extend()
Метод extend() добавляет все элементы из другого списка (или любого итерируемого объекта) в конец текущего списка:
primary_colors = ["red", "blue", "yellow"]
secondary_colors = ["green", "orange", "purple"]
# Добавляем все secondary_colors в primary_colors
primary_colors.extend(secondary_colors)
print(primary_colors)
# Output: ['red', 'blue', 'yellow', 'green', 'orange', 'purple']Это отличается от append(), который добавил бы весь список как один элемент:
colors1 = ["red", "blue"]
colors2 = ["green", "orange"]
# Используем append (добавляет список как один элемент)
colors1.append(colors2)
print(colors1) # Output: ['red', 'blue', ['green', 'orange']]
# Используем extend (добавляет каждый элемент по отдельности)
colors3 = ["red", "blue"]
colors3.extend(colors2)
print(colors3) # Output: ['red', 'blue', 'green', 'orange']14.3.7) Сортировка списков с sort() и sorted()
Python предоставляет два способа сортировать списки. Метод sort() сортирует список на месте(in place) (изменяя оригинал):
scores = [78, 95, 85, 92, 88]
scores.sort()
print(scores) # Output: [78, 85, 88, 92, 95]Чтобы отсортировать по убыванию, используйте параметр reverse:
scores = [78, 95, 85, 92, 88]
scores.sort(reverse=True)
print(scores) # Output: [95, 92, 88, 85, 78]Функция sorted() (которую мы подробнее рассмотрим в главе 38) создаёт новый отсортированный список(new sorted list), не изменяя исходный:
original = [78, 95, 85, 92, 88]
sorted_scores = sorted(original)
print(original) # Output: [78, 95, 85, 92, 88]
print(sorted_scores) # Output: [78, 85, 88, 92, 95]Сортировка работает и со строками, используя алфавитный порядок:
names = ["Charlie", "Alice", "Diana", "Bob"]
names.sort()
print(names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']14.3.8) Разворот списков с reverse()
Метод reverse() разворачивает список на месте(in place):
numbers = [1, 2, 3, 4, 5]
numbers.reverse()
print(numbers) # Output: [5, 4, 3, 2, 1]Это отличается от сортировки в обратном порядке — reverse() просто переворачивает текущий порядок, каким бы он ни был:
mixed = [3, 1, 4, 1, 5]
mixed.reverse()
print(mixed) # Output: [5, 1, 4, 1, 3]Помните, что развернуть список можно и с помощью среза: list[::-1]. Разница в том, что срез создаёт новый список, а reverse() изменяет исходный.
14.3.9) Поиск элементов с index() и count()
Метод index() возвращает позицию первого вхождения значения:
students = ["Alice", "Bob", "Charlie", "Diana", "Bob"]
# Находим, где находится "Charlie"
position = students.index("Charlie")
print(f"Charlie is at index {position}") # Output: Charlie is at index 2
# Находим первого "Bob"
bob_position = students.index("Bob")
print(f"Bob is at index {bob_position}") # Output: Bob is at index 1Если значения не существует, index() вызывает ValueError:
students = ["Alice", "Bob", "Charlie"]
# WARNING: Попытка найти несуществующее значение — только для демонстрации
# PROBLEM: 'Eve' нет в списке
# position = students.index("Eve") # ValueError: 'Eve' is not in listМетод count() возвращает, сколько раз встречается значение:
numbers = [1, 2, 3, 2, 4, 2, 5]
twos = numbers.count(2)
print(f"The number 2 appears {twos} times") # Output: The number 2 appears 3 times
# count может вернуть 0, если элемента не существует
sixes = numbers.count(6)
print(f"The number 6 appears {sixes} times") # Output: The number 6 appears 0 times14.3.10) Очистка всех элементов с clear()
Метод clear() удаляет все элементы из списка, оставляя его пустым:
cart = ["Milk", "Bread", "Eggs", "Butter"]
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
cart.clear()
print(cart) # Output: []
print(len(cart)) # Output: 0Это эквивалентно присваиванию пустого списка, но clear() более явно выражает намерение.
14.4) Удаление элементов списка с del
14.4.1) Использование del для удаления элементов по индексу
Оператор del может удалять элементы списка по конкретным индексам:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']
# Удаляем элемент по индексу 2
del students[2]
print(students) # Output: ['Alice', 'Bob', 'Diana', 'Eve']В отличие от pop(), del не возвращает удалённое значение — он просто удаляет его. Это полезно, когда вы хотите удалить элемент, но не собираетесь использовать его:
scores = [85, 92, 78, 95, 88]
# Удаляем самый низкий результат (по индексу 2)
del scores[2]
print(scores) # Output: [85, 92, 95, 88]Также можно использовать отрицательные индексы с del:
tasks = ["Task 1", "Task 2", "Task 3", "Task 4"]
# Удаляем последнюю задачу
del tasks[-1]
print(tasks) # Output: ['Task 1', 'Task 2', 'Task 3']14.4.2) Удаление срезов с del
Оператор del может удалять целые срезы сразу:
numbers = [10, 20, 30, 40, 50, 60, 70]
print(numbers) # Output: [10, 20, 30, 40, 50, 60, 70]
# Удаляем элементы с индекса 2 по 4 (индексы 2, 3, 4)
del numbers[2:5]
print(numbers) # Output: [10, 20, 60, 70]Это особенно полезно для удаления диапазонов элементов:
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Удаляем первые три элемента
del data[:3]
print(data) # Output: [4, 5, 6, 7, 8, 9, 10]
# Удаляем последние два элемента
del data[-2:]
print(data) # Output: [4, 5, 6, 7, 8]Вы даже можете удалить каждый второй элемент, используя шаг в срезе:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Удаляем каждый второй элемент
del numbers[::2]
print(numbers) # Output: [1, 3, 5, 7, 9]14.4.3) Сравнение del, remove() и pop()
Давайте уточним, когда использовать каждый способ удаления:
# Пример списка для сравнения
items = ["apple", "banana", "cherry", "date", "elderberry"]
# Используйте remove(), когда вы знаете ЗНАЧЕНИЕ, которое нужно удалить
items_copy1 = items.copy()
items_copy1.remove("cherry") # Удаляет первый "cherry"
print(items_copy1) # Output: ['apple', 'banana', 'date', 'elderberry']
# Используйте pop(), когда вы знаете ИНДЕКС и вам нужно значение
items_copy2 = items.copy()
removed_item = items_copy2.pop(2) # Удаляет и возвращает элемент по индексу 2
print(f"Removed: {removed_item}") # Output: Removed: cherry
print(items_copy2) # Output: ['apple', 'banana', 'date', 'elderberry']
# Используйте del, когда вы знаете ИНДЕКС, но значение не нужно
items_copy3 = items.copy()
del items_copy3[2] # Просто удаляет элемент по индексу 2
print(items_copy3) # Output: ['apple', 'banana', 'date', 'elderberry']14.5) Перебор списков с циклами for
14.5.1) Базовый перебор списка
Одна из самых распространённых операций со списками — обработка каждого элемента по порядку. Цикл for (который мы изучили в главе 12) идеально подходит для этого:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Обрабатываем каждого студента
for student in students:
print(f"Hello, {student}!")
# Output:
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!
# Hello, Diana!Переменная цикла (student в данном случае) по очереди принимает каждое значение из списка, по одному, в порядке следования. Вы можете назвать эту переменную как угодно, лишь бы это имело смысл:
scores = [85, 92, 78, 95, 88]
# Вычисляем и выводим оценку (grade) для каждого результата
for score in scores:
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "C"
print(f"Score {score} is a {grade}")
# Output:
# Score 85 is a B
# Score 92 is a A
# Score 78 is a C
# Score 95 is a A
# Score 88 is a B14.5.2) Обработка соответствующих элементов из нескольких списков
Иногда нужно работать со связанными данными, хранящимися в отдельных списках. Мы подробно изучим функцию zip() в главе 38, а пока — краткий пример того, как она помогает обрабатывать соответствующие элементы:
# Мы изучим zip() в главе 38, а пока вот простой пример
students = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
# Обрабатываем соответствующие пары
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 85
# Bob scored 92
# Charlie scored 78Функция zip() объединяет элементы из нескольких списков в пары, что полезно, когда у вас есть связанные данные в отдельных списках. Мы подробно рассмотрим это и другие инструменты итерации в главе 38.
14.6) Копирование списков и предотвращение общих ссылок
14.6.1) Понимание ссылок на списки
Когда вы присваиваете список переменной, Python не создаёт копию списка — он создаёт ссылку(reference) на тот же объект списка в памяти. Это означает, что несколько переменных могут ссылаться на один и тот же список:
original = [1, 2, 3]
reference = original # Обе переменные указывают на ОДИН и тот же список
# Изменение через одну переменную влияет на другую
reference.append(4)
print(original) # Output: [1, 2, 3, 4]
print(reference) # Output: [1, 2, 3, 4]Это поведение может удивить, если вы ожидаете, что reference — независимая копия. Давайте посмотрим, почему это важно:
# Сценарий: вы хотите отслеживать изменения корзины покупок
cart = ["Milk", "Bread"]
backup = cart # Пытаемся сохранить исходное состояние
# Добавляем больше товаров
cart.append("Eggs")
cart.append("Butter")
# Проверяем "backup"
print(backup) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']Резервная копия тоже изменилась! Это потому, что backup и cart — два имени для одного и того же объекта списка.
14.6.2) Создание независимых копий с помощью срезов
Чтобы создать настоящую независимую копию, используйте срез [:]:
original = [1, 2, 3]
copy = original[:] # Создаёт НОВЫЙ список с тем же содержимым
# Изменение копии не влияет на оригинал
copy.append(4)
print(original) # Output: [1, 2, 3]
print(copy) # Output: [1, 2, 3, 4]Теперь исправим пример с корзиной покупок:
cart = ["Milk", "Bread"]
backup = cart[:] # Создаём независимую копию
# Добавляем больше товаров в cart
cart.append("Eggs")
cart.append("Butter")
# backup остаётся неизменным
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
print(backup) # Output: ['Milk', 'Bread']14.6.3) Создание копий методом copy()
У списков также есть метод copy(), который делает то же самое, что и [:]:
original = [10, 20, 30]
copy = original.copy()
copy.append(40)
print(original) # Output: [10, 20, 30]
print(copy) # Output: [10, 20, 30, 40]И [:], и copy() создают поверхностные копии(shallow copies), о которых мы поговорим дальше.
14.6.4) Ограничение поверхностного копирования
И [:], и copy() создают поверхностные копии(shallow copies). Это означает, что они копируют структуру списка, но если список содержит другие изменяемые объекты (например, другие списки), то эти внутренние объекты всё ещё остаются общими:
# Список, содержащий списки
original = [[1, 2], [3, 4], [5, 6]]
copy = original[:]
# Изменение внешней структуры списка независимое
copy.append([7, 8])
print(original) # Output: [[1, 2], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2], [3, 4], [5, 6], [7, 8]]
# Но изменение внутреннего списка влияет на оба!
copy[0].append(99)
print(original) # Output: [[1, 2, 99], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2, 99], [3, 4], [5, 6], [7, 8]]Почему так происходит? Потому что поверхностная копия создаёт новый внешний список, но внутренние списки всё ещё являются общими ссылками:
Для вложенных структур вам понадобится глубокая копия(deep copy), с которой мы познакомимся, когда будем изучать модуль copy в последующих главах. Пока просто учитывайте, что поверхностные копии отлично работают для списков неизменяемых элементов (числа, строки, кортежи), но требуют осторожности со вложенными изменяемыми структурами.
14.6.5) Когда общие ссылки полезны
Иногда вы хотите, чтобы несколько переменных ссылались на один и тот же список. Это полезно, когда нужно изменять список из разных частей кода:
# Функция, которая изменяет список на месте
def add_bonus_points(scores, bonus):
for i in range(len(scores)):
scores[i] = scores[i] + bonus
# Исходный список изменяется
student_scores = [85, 92, 78]
add_bonus_points(student_scores, 5)
print(student_scores) # Output: [90, 97, 83]Это работает, потому что функция получает ссылку на исходный список, а не его копию. Мы подробнее рассмотрим это, когда будем изучать функции в деталях в части V.
14.7) Использование enumerate() при переборе списков
14.7.1) Необходимость и индекса, и значения
Иногда при переборе списка вам нужны и индекс, и значение. Один из подходов — использовать range(len(list)):
students = ["Alice", "Bob", "Charlie", "Diana"]
for i in range(len(students)):
print(f"Student {i}: {students[i]}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: DianaЭто работает, но выглядит не слишком элегантно. Вам нужно использовать students[i], чтобы получить каждое значение, что читается хуже, чем прямой перебор значений.
14.7.2) Использование enumerate() для более чистого кода
Функция enumerate() предоставляет более удачное решение. Она возвращает и индекс, и значение для каждого элемента:
students = ["Alice", "Bob", "Charlie", "Diana"]
for index, student in enumerate(students):
print(f"Student {index}: {student}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: DianaСинтаксис for index, value in enumerate(list) распаковывает каждую пару, которую создаёт enumerate(). Это гораздо более читаемо, чем использование range(len()).
14.7.3) Запуск enumerate() с другого числа
По умолчанию enumerate() начинает счёт с 0. Вы можете указать другое начальное число с помощью параметра start:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Начинаем счёт с 1 вместо 0
for position, student in enumerate(students, start=1):
print(f"Position {position}: {student}")
# Output:
# Position 1: Alice
# Position 2: Bob
# Position 3: Charlie
# Position 4: DianaЭто полезно, когда вы хотите показывать «человеко-удобную» нумерацию (с 1), а не «программистскую» индексацию (с 0).
Практические примеры с enumerate()
Вот практический пример вывода нумерованного меню:
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit14.7.4) Изменение списков с enumerate()
Вы можете использовать enumerate(), когда нужно изменять элементы списка на основе их позиции:
# Добавляем позиционный бонус к результатам
scores = [85, 92, 78, 95, 88]
for index, score in enumerate(scores):
# Первый студент получает 5 бонусных баллов, второй получает 4 и т. д.
bonus = 5 - index
if bonus > 0:
scores[index] = score + bonus
print(scores) # Output: [90, 96, 81, 97, 89]14.8) Распространённые паттерны работы со списками: поиск, фильтрация и агрегирование данных
14.8.1) Поиск элементов в списках
Одна из самых распространённых задач — проверить, содержит ли список определённый элемент. Оператор in (который мы изучили в главе 7) делает это простым:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Проверяем, есть ли студент в списке
if "Charlie" in students:
print("Charlie is enrolled") # Output: Charlie is enrolled
if "Eve" not in students:
print("Eve is not enrolled") # Output: Eve is not enrolledЧтобы найти позицию элемента, используйте метод index() (рассмотрен в разделе 14.3.9), но помните, что сначала нужно проверить, существует ли элемент:
scores = [85, 92, 78, 95, 88]
target_score = 95
if target_score in scores:
position = scores.index(target_score)
print(f"Score {target_score} found at index {position}")
# Output: Score 95 found at index 3
else:
print(f"Score {target_score} not found")14.8.2) Нахождение максимального и минимального значений
Встроенные функции Python max() и min() работают со списками:
scores = [85, 92, 78, 95, 88, 91, 76]
highest_score = max(scores)
lowest_score = min(scores)
print(f"Highest score: {highest_score}") # Output: Highest score: 95
print(f"Lowest score: {lowest_score}") # Output: Lowest score: 7614.8.3) Вычисление агрегатов: сумма, среднее и количество
Вычисление сумм и средних значений — базовая операция со списками:
scores = [85, 92, 78, 95, 88, 91, 76, 89]
# Вычисляем сумму и среднее
total = sum(scores)
count = len(scores)
average = total / count
print(f"Total: {total}") # Output: Total: 694
print(f"Count: {count}") # Output: Count: 8
print(f"Average: {average:.2f}") # Output: Average: 86.75Вот практический пример вычисления общей стоимости корзины покупок:
cart_items = ["Milk", "Bread", "Eggs", "Butter", "Cheese"]
prices = [3.99, 2.49, 4.99, 5.49, 6.99]
# Вычисляем общую стоимость
total_cost = sum(prices)
item_count = len(cart_items)
print(f"Items in cart: {item_count}")
print(f"Total cost: ${total_cost:.2f}")
# Output:
# Items in cart: 5
# Total cost: $23.9514.9) Изменяемость списков и truthiness в условиях
14.9.1) Понимание изменяемости списков на практике
Мы видели на протяжении этой главы, что списки изменяемы(mutable) — их можно менять после создания. Именно эта изменяемость делает списки такими мощными для хранения и обработки коллекций данных. Давайте закрепим понимание с помощью комплексного примера:
# Начинаем с пустого списка задач
tasks = []
print(f"Initial tasks: {tasks}") # Output: Initial tasks: []
# Добавляем задачи
tasks.append("Write code")
tasks.append("Test code")
tasks.append("Deploy code")
print(f"After adding: {tasks}")
# Output: After adding: ['Write code', 'Test code', 'Deploy code']
# Вставляем срочную задачу в начало
tasks.insert(0, "Review requirements")
print(f"After inserting: {tasks}")
# Output: After inserting: ['Review requirements', 'Write code', 'Test code', 'Deploy code']
# Завершаем и удаляем первую задачу
completed = tasks.pop(0)
print(f"Completed: {completed}") # Output: Completed: Review requirements
print(f"Remaining: {tasks}")
# Output: Remaining: ['Write code', 'Test code', 'Deploy code']
# Изменяем задачу
tasks[1] = "Test code thoroughly"
print(f"After modifying: {tasks}")
# Output: After modifying: ['Write code', 'Test code thoroughly', 'Deploy code']14.9.2) Изменяемость vs неизменяемость: списки vs строки
Важно понимать различие между изменяемыми списками и неизменяемыми строками. Со строками операции создают новые строки, а не модифицируют исходную:
# Строки неизменяемы
text = "hello"
text.upper() # Создаёт новую строку, не изменяет исходную
print(text) # Output: hello (unchanged)
# Чтобы "изменить" строку, нужно переприсвоить
text = text.upper()
print(text) # Output: HELLO
# Списки изменяемы
numbers = [1, 2, 3]
numbers.append(4) # Изменяет список на месте
print(numbers) # Output: [1, 2, 3, 4] (changed)Это различие влияет на то, как вы работаете с этими типами:
# Операции со строками требуют переприсваивания
name = "alice"
name = name.capitalize() # Нужно переприсвоить, чтобы увидеть изменение
print(name) # Output: Alice
# Операции со списками меняют данные на месте
scores = [85, 92, 78]
scores.append(95) # Переприсваивание не требуется
print(scores) # Output: [85, 92, 78, 95]14.9.3) Использование списков в булевом контексте
У списков есть truthiness: пустой список считается False, а любой непустой список считается True. Это полезно в условных операторах:
# Пустой список — falsy
empty_cart = []
if empty_cart:
print("Cart has items")
else:
print("Cart is empty") # Output: Cart is empty
# Непустой список — truthy
cart_with_items = ["Milk", "Bread"]
if cart_with_items:
print("Cart has items") # Output: Cart has itemsЭтот паттерн часто используется, чтобы проверить, есть ли в списке элементы, прежде чем обрабатывать их:
students = ["Alice", "Bob", "Charlie"]
if students:
print(f"We have {len(students)} students")
for student in students:
print(f" - {student}")
else:
print("No students enrolled")
# Output:
# We have 3 students
# - Alice
# - Bob
# - Charlie14.9.4) Практический паттерн: обработка до опустошения
Truthiness списков позволяет использовать полезный паттерн обработки элементов до тех пор, пока список не станет пустым:
# Обрабатываем задачи, пока не останется ни одной
tasks = ["Task 1", "Task 2", "Task 3"]
while tasks: # Продолжаем, пока список не пуст
current_task = tasks.pop(0)
print(f"Processing: {current_task}")
print("All tasks completed!")
# Output:
# Processing: Task 1
# Processing: Task 2
# Processing: Task 3
# All tasks completed!14.9.5) Проверка пустых списков: явная vs неявная
Есть два способа проверить, пуст ли список:
items = []
# Неявная проверка (Pythonic)
if not items:
print("List is empty") # Output: List is empty
# Явная проверка (тоже корректно)
if len(items) == 0:
print("List is empty") # Output: List is emptyНеявная проверка (if not items:) обычно предпочтительнее в Python, потому что она короче и работает с любым типом коллекции. Однако оба подхода корректны, и вы встретите оба варианта в реальном коде.
14.9.6) Изменяемость и поведение функций
Когда вы передаёте список в функцию (что мы подробно рассмотрим в части V), функция получает ссылку на тот же объект списка. Это означает, что функция может изменить исходный список:
def add_item(shopping_list, item):
shopping_list.append(item)
print(f"Added {item}")
# Исходный список изменяется
cart = ["Milk", "Bread"]
print(f"Before: {cart}") # Output: Before: ['Milk', 'Bread']
add_item(cart, "Eggs") # Output: Added Eggs
print(f"After: {cart}") # Output: After: ['Milk', 'Bread', 'Eggs']Это поведение отличается от неизменяемых типов, таких как строки и числа, где исходное значение не может быть изменено функцией. Понимание этого различия критически важно для написания корректных программ.
Списки — одна из самых фундаментальных и универсальных структур данных Python. Они предоставляют упорядоченную, изменяемую коллекцию, которая может расти и уменьшаться по мере необходимости, что делает их идеальными для хранения и обработки последовательностей связанных данных. Вы научились создавать списки, получать доступ к их элементам через индексацию и срезы, изменять их различными методами, эффективно перебирать их и понимать их изменяемую природу.
Паттерны, которые мы рассмотрели — поиск, фильтрация, агрегирование и преобразование данных — формируют основу работы с коллекциями в Python. По мере продолжения обучения вы откроете ещё более мощные способы работы со списками, включая генераторы списков (list comprehensions) (глава 35) и продвинутые техники итерации (главы 36–37). Но основы, которые вы освоили в этой главе, хорошо послужат вам на всём пути изучения программирования на Python.