Списки, словари и множества: работа с наборами данных
До сих пор одна переменная хранила одно значение: возраст, имя, цену. Этого хватает, пока данных мало. Но настоящие задачи выглядят иначе — не «цена товара», а «цены всех товаров в чеке». Не «оценка», а «оценки всего класса».
Хранить такое в отдельных переменных невозможно физически. Тридцать учеников — тридцать переменных student1, student2, …, и любое действие вроде «посчитай средний балл» превращается в тридцать строк кода. А если завтра учеников станет тридцать один — придётся переписывать программу.
Эта статья — про инструменты, которые решают проблему: список, словарь и множество. Их называют коллекциями или структурами данных — это просто способы хранить много значений под одним именем. Освоив их, вы перестанете писать однотипный код и начнёте писать программы, которые работают с любым объёмом данных.
Три инструмента и три разные задачи
Все три хранят несколько значений, но отвечают на разные вопросы.
Не пытайтесь запомнить всё сразу — к концу статьи это уляжется. Пока достаточно одной мысли: выбор структуры зависит от того, как вы собираетесь искать в ней данные.
Список: когда важен порядок
Список (по-английски list) — упорядоченный набор значений. Ближайшая бытовая аналогия — список покупок на бумажке: пункты идут сверху вниз, у каждого своё место, можно дописать в конец, можно вычеркнуть.
Список записывается в квадратных скобках, значения разделяются запятыми.
shopping = ["хлеб", "молоко", "яйца", "сыр", "кофе"]
print(shopping) # печатаем весь список целиком
print(len(shopping)) # len() — сколько элементов в списке
Ожидаемый вывод:
['хлеб', 'молоко', 'яйца', 'сыр', 'кофе']
5
Разберём построчно:
shopping = [...]— создали список из пяти строк и дали ему имя. Одно имя, пять значений.print(shopping)— Python печатает список так, как его можно было бы записать в коде. Кавычки одинарные — это просто его манера, разницы с двойными нет.len(shopping)— встроенная функцияlen(от английского length, «длина») возвращает количество элементов.
Список может хранить что угодно и даже вперемешку: числа, строки, True/False. Но на практике так почти не делают — обычно список хранит однородные вещи: все цены, все имена.
Обращение к элементу: индексы
Каждый элемент имеет индекс — свой порядковый номер. И вот первая вещь, которая удивляет всех новичков: нумерация начинается с нуля, а не с единицы.
shopping = ["хлеб", "молоко", "яйца", "сыр", "кофе"]
print(shopping[0]) # первый элемент
print(shopping[2]) # третий элемент
print(shopping[-1]) # последний элемент
print(shopping[-2]) # предпоследний
хлеб
яйца
кофе
сыр
shopping[0]— квадратные скобки после имени означают «возьми элемент с таким номером». Ноль — это первый элемент.shopping[2]— третий, потому что счёт идёт 0, 1, 2.shopping[-1]— отрицательный индекс считает с конца. Очень удобно: чтобы взять последний элемент, не нужно знать длину списка.
Почему с нуля? Исторически индекс означал не «какой по счёту», а «на сколько шагов отступить от начала». От начала до первого элемента отступать не нужно — отсюда ноль. Логика есть, но привыкать всё равно приходится. Полезная формула: индекс последнего элемента всегда len(список) - 1.
Индекс можно не только читать, но и присваивать:
shopping[1] = "кефир" # передумали насчёт молока
print(shopping) # ['хлеб', 'кефир', 'яйца', 'сыр', 'кофе']
Срезы: взять кусок списка
Иногда нужен не один элемент, а несколько подряд. Для этого есть срез (slice): в скобках указывают начало и конец через двоеточие.
numbers = [10, 20, 30, 40, 50, 60]
print(numbers[1:4]) # с 1-го по 3-й включительно
print(numbers[:3]) # с начала до 3-го (не включая его)
print(numbers[3:]) # с 3-го и до конца
print(numbers[-2:]) # последние два
[20, 30, 40]
[10, 20, 30]
[40, 50, 60]
[50, 60]
Ключевой момент, на котором спотыкаются буквально все: правая граница не входит в срез. numbers[1:4] берёт элементы 1, 2, 3, но не 4.
Звучит нелогично, но следствие приятное: длина среза равна разнице границ (4 - 1 = 3 элемента), а два среза [:3] и [3:] вместе дают ровно исходный список — без пересечений и пропусков.
Изменение списка: добавить, вставить, удалить
Список — изменяемая структура: его можно править после создания.
tasks = ["купить хлеб", "позвонить маме"]
tasks.append("оплатить счёт") # добавить в конец
tasks.insert(0, "сделать зарядку") # вставить на позицию 0, остальные сдвинутся
tasks.remove("позвонить маме") # удалить по значению
print(tasks)
done = tasks.pop() # удалить последний И вернуть его
print(done)
print(tasks)
['сделать зарядку', 'купить хлеб', 'оплатить счёт']
оплатить счёт
['сделать зарядку', 'купить хлеб']
Обратите внимание на запись через точку — tasks.append(...). Это метод: функция, которая принадлежит конкретному объекту и работает именно с ним. append не существует сам по себе, он всегда чей-то. Почему функции бывают «привязаны» к данным — в статье про объекты и классы.
Важная деталь: эти методы меняют список на месте и возвращают None (то есть «ничего»). Отсюда классическая ошибка:
tasks = ["а", "б"]
tasks = tasks.append("в") # НЕПРАВИЛЬНО
print(tasks) # None
Список-то изменился, но результатом append было «ничего», и это «ничего» затёрло имя tasks. Правильно — просто tasks.append("в"), без присваивания.
Перебор списка циклом
Главная причина, по которой списки так удобны: их можно перебирать циклом — обрабатывать каждый элемент, не зная заранее, сколько их.
prices = [120, 89, 45, 230]
total = 0
for price in prices: # берём каждый элемент по очереди
total = total + price # накапливаем сумму
print("Итого:", total) # Итого: 574
Строка for price in prices: читается почти по-русски: «для каждой цены в списке цен». Переменная price на каждом обороте указывает на очередной элемент. Этот код одинаково работает и для четырёх товаров, и для четырёх тысяч.
Если нужен ещё и номер элемента, используйте enumerate:
for i, price in enumerate(prices):
print(i, "-", price) # 0 - 120, затем 1 - 89, и так далее
Часто нужные операции
scores = [7, 3, 9, 3, 5]
print(sum(scores)) # 27 — сумма
print(min(scores), max(scores)) # 3 9 — наименьшее и наибольшее
print(sum(scores) / len(scores)) # 5.4 — среднее
print(3 in scores) # True — есть ли такое значение
print(sorted(scores)) # [3, 3, 5, 7, 9] — НОВЫЙ список
print(scores) # [7, 3, 9, 3, 5] — исходный не тронут
scores.sort() # а вот это сортирует на месте
print(scores) # [3, 3, 5, 7, 9]
Запомните различие: sorted(x) — функция, которая возвращает новый список; x.sort() — метод, который меняет существующий и возвращает None. Та же ловушка, что с append.
Кортеж: список, который нельзя менять
У списка есть близкий родственник — кортеж (tuple). Записывается в круглых скобках и отличается ровно одним: изменить его нельзя.
point = (55.75, 37.62) # координаты Москвы: широта и долгота
print(point[0]) # 55.75
point[0] = 0 # попытка изменить
Traceback (most recent call last):
File "example.py", line 4, in <module>
point[0] = 0
TypeError: 'tuple' object does not support item assignment
Сообщение переводится дословно: «объект типа tuple не поддерживает присваивание элементу». Зачем нужна структура, которую нельзя менять? Неизменяемость — это гарантия. Если данные меняться не должны (координаты, дата рождения, размеры экрана), кортеж защищает от случайной правки. И только неизменяемые значения можно класть в множества и использовать как ключи словаря — об этом ниже.
Словарь: когда важно имя, а не номер
Представьте телефонную книгу. Вам не приходит в голову спрашивать «какой номер записан 47-м по счёту?» — вы ищете по имени. Именно для таких задач нужен словарь (dict).
Словарь хранит пары ключ → значение. Ключ — то, по чему ищем; значение — то, что находим.
prices = {
"хлеб": 60,
"молоко": 95,
"кофе": 420,
}
print(prices["кофе"]) # 420 — ищем по ключу, а не по номеру
print(len(prices)) # 3 — сколько пар в словаре
- Фигурные скобки
{}— признак словаря. "хлеб": 60— пара: ключ"хлеб", значение60. Двоеточие читается как «стоит».prices["кофе"]— квадратные скобки, как у списка, но внутри не номер, а ключ.- Запятая после последней пары не обязательна, но её принято ставить: удобнее дописывать строки.
Добавление, изменение, удаление
prices["чай"] = 150 # ключа не было — пара добавилась
prices["хлеб"] = 65 # ключ был — значение заменилось
del prices["молоко"] # удалили пару целиком
print(prices) # {'хлеб': 65, 'кофе': 420, 'чай': 150}
Одна строка prices[ключ] = значение делает и добавление, и обновление — Python сам разбирается, что нужно. Ключи уникальны: второго "хлеб" быть не может, новое значение вытеснит старое.
Начиная с Python 3.7 словарь сохраняет порядок добавления пар, поэтому вывод предсказуем. Но полагаться на порядок как на способ доступа не стоит — словарь создан для поиска по ключу.
Безопасное чтение: get
Обращение к несуществующему ключу — это ошибка, программа падает:
print(prices["сахар"])
Traceback (most recent call last):
File "example.py", line 1, in <module>
print(prices["сахар"])
KeyError: 'сахар'
KeyError дословно — «ошибка ключа», а рядом Python показывает, какого именно ключа не нашёл. Если ключа может не быть, используйте метод get:
print(prices.get("сахар")) # None — нет ключа, но программа не упала
print(prices.get("сахар", 0)) # 0 — можно задать значение по умолчанию
print("сахар" in prices) # False — просто проверить наличие
Оператор in для словаря проверяет ключи, а не значения — это частая точка путаницы.
Перебор словаря
prices = {"хлеб": 65, "кофе": 420, "чай": 150}
for name in prices: # по умолчанию перебираются КЛЮЧИ
print(name)
for name, price in prices.items(): # а так — сразу пары
print(name, "стоит", price, "руб.")
print(sum(prices.values())) # values() — все значения
хлеб
кофе
чай
хлеб стоит 65 руб.
кофе стоит 420 руб.
чай стоит 150 руб.
635
prices.items() выдаёт пары, а запись for name, price in ... сразу раскладывает каждую пару на две переменные. Это самый частый способ обхода словаря — запомните его.
Почему поиск в словаре быстрый
Чтобы найти значение в списке, Python просматривает элементы подряд: в списке из миллиона имён ему в худшем случае придётся заглянуть в миллион ячеек. Словарь работает иначе.
Словарь пропускает ключ через хеш-функцию — расчёт, который превращает любое значение в число. Это число подсказывает, в какой ячейке памяти лежит нужная пара. Поиск получается почти мгновенным и — вот главное — не зависит от размера словаря. В словаре из миллиона записей поиск занимает столько же, сколько в словаре из десяти. На языке оценки сложности: у словаря O(1), у поиска в списке — O(n).
Аналогия с гардеробом и номерками помогает, но у неё есть честная граница: в гардеробе номерок выдаёт человек, а здесь номер вычисляется из самого ключа. Именно поэтому ключ обязан быть неизменяемым — строкой, числом или кортежем. Если бы ключ можно было изменить после вставки, вычисленный номер перестал бы совпадать с реальным местом, и значение потерялось бы. Отсюда ошибка при попытке использовать список как ключ:
d = {}
d[["а", "б"]] = 1
TypeError: unhashable type: 'list'
Дословно: «тип list нехешируемый», то есть от него нельзя посчитать хеш. Решение — взять кортеж: d[("а", "б")] = 1.
Множество: только уникальные значения
Множество (set) — набор без повторов и без порядка. Аналогия — список приглашённых на вечеринку: человек либо в списке, либо нет; записать его дважды бессмысленно; порядок записи ничего не значит.
visitors = {"Аня", "Борис", "Аня", "Вера"}
print(visitors) # {'Вера', 'Аня', 'Борис'} — порядок может быть любым!
print(len(visitors)) # 3 — дубликат исчез сам
print("Борис" in visitors) # True — быстрая проверка принадлежности
Порядок в вашем выводе может отличаться — это нормально и не является ошибкой. Множество не обещает порядок вообще. Если нужен предсказуемый вывод, оборачивайте в sorted(...).
Самое частое применение — убрать повторы из списка:
words = ["кот", "пёс", "кот", "уж", "пёс", "кот"]
unique = set(words) # превращаем список в множество
print(len(words), "слов, из них разных:", len(unique))
print(sorted(unique)) # сортируем, чтобы вывод был стабильным
6 слов, из них разных: 3
['кот', 'пёс', 'уж']
Множества умеют то, чему учили на уроках математики: объединение, пересечение, разность.
python_devs = {"Аня", "Борис", "Вера"}
sql_devs = {"Борис", "Вера", "Глеб"}
print(sorted(python_devs & sql_devs)) # & — пересечение: знают оба
print(sorted(python_devs | sql_devs)) # | — объединение: знают хоть что-то
print(sorted(python_devs - sql_devs)) # - — разность: только Python
['Борис', 'Вера']
['Аня', 'Борис', 'Вера', 'Глеб']
['Аня']
Одна ловушка: пустое множество нельзя записать как {} — эти скобки уже заняты под пустой словарь. Пишите set().
Как выбрать структуру
Это решение принимается почти механически — по тому, как вы будете обращаться к данным.
несколько значений"] --> B{"Искать
по имени/ключу?"} B -- да --> C["Словарь dict"] B -- нет --> D{"Важны ли повторы
и порядок?"} D -- "нужны только
уникальные" --> E["Множество set"] D -- "порядок важен,
повторы допустимы" --> F{"Данные будут
меняться?"} F -- да --> G["Список list"] F -- "нет, набор
фиксированный" --> H["Кортеж tuple"]
| Структура | Скобки | Порядок | Повторы | Изменяемая | Поиск элемента |
|---|---|---|---|---|---|
list |
[ ] |
да | да | да | медленный, O(n) |
tuple |
( ) |
да | да | нет | медленный, O(n) |
dict |
{k: v} |
по добавлению | ключи уникальны | да | быстрый, O(1) |
set |
{ } |
нет | нет | да | быстрый, O(1) |
Не бойтесь ошибиться с выбором на старте. Переделать список в словарь — дело нескольких минут, а понимание приходит из практики, а не из таблицы.
Типичные ошибки новичков
1. IndexError: выход за границы
items = ["а", "б", "в"]
print(items[3])
IndexError: list index out of range
«Индекс списка вне диапазона». В списке из трёх элементов индексы — 0, 1 и 2; тройки там нет. Помните: последний индекс — это len(items) - 1.
2. Присваивание списка не делает копию
В статье о переменных мы говорили, что имя — это наклейка на значение. Со списками это становится особенно заметно:
a = [1, 2, 3]
b = a # НЕ копия! вторая наклейка на тот же список
b.append(4)
print(a) # [1, 2, 3, 4]
Список один, наклеек две — изменение видно через обе. Если нужна именно копия, скажите об этом явно: b = a.copy() (или b = a[:] — срез целиком тоже создаёт новый список). Это одна из самых коварных ошибок: программа не падает, а тихо портит данные.
3. Изменение списка прямо во время перебора
numbers = [1, 2, 3, 4]
for n in numbers:
if n % 2 == 0:
numbers.remove(n) # опасно!
print(numbers) # [1, 3]
Кажется, всё сработало — но это случайность. Удаляя элемент, вы сдвигаете остальные, а цикл продолжает считать по старым позициям и часть элементов пропускает. Проверьте на [2, 2, 3] — одна двойка останется. Правильный подход — собирать новый список:
numbers = [1, 2, 3, 4]
odd = []
for n in numbers:
if n % 2 != 0:
odd.append(n)
print(odd) # [1, 3]
4. Одинаковые скобки, разный смысл
spisok[0] — это «нулевой по счёту». slovar[0] — это «значение по ключу 0», и если такого ключа нет, будет KeyError. У списка внутри скобок всегда номер, у словаря — ключ. И если данные приходят извне (от пользователя, из файла), нельзя рассчитывать, что ключ есть: привычка писать data.get("поле", по_умолчанию) избавляет от половины падений.
5. Ожидание порядка там, где его нет
print({1, 2, 3} == {3, 2, 1}) # True — у множеств порядка нет
print([1, 2, 3] == [3, 2, 1]) # False — у списков порядок часть содержимого
Собираем всё вместе: анализ текста
Небольшая программа, где все три структуры работают вместе. Задача — посчитать, какие слова встречаются в тексте чаще всего.
text = "кот и пёс и снова кот и кот"
words = text.split() # split() режет строку на список слов по пробелам
print(words)
counts = {} # пустой словарь: слово -> сколько раз встретилось
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
unique_words = set(words) # множество: просто разные слова
print("Разных слов:", len(unique_words))
# сортируем пары по количеству, от большего к меньшему
top = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
for word, number in top:
print(word, "-", number)
['кот', 'и', 'пёс', 'и', 'снова', 'кот', 'и', 'кот']
{'кот': 3, 'и': 3, 'пёс': 1, 'снова': 1}
Разных слов: 4
кот - 3
и - 3
пёс - 1
снова - 1
Что тут происходит:
text.split()— метод строк, который разрезает её по пробелам и возвращает список слов.- Словарь
countsкопит счётчики: ключ — слово, значение — сколько раз оно встретилось. Строкуcounts[word] = counts.get(word, 0) + 1прочитайте вслух: «возьми текущее значение или ноль, если его ещё нет, прибавь единицу и запиши обратно». Это классический приём, который вы будете использовать постоянно. sorted(..., key=..., reverse=True)сортирует пары. Аргументkeyговорит, по чему сортировать:lambda pair: pair[1]означает «бери из пары элемент с индексом 1», то есть количество.lambda— короткая безымянная функция; пока достаточно воспринимать эту строку как готовый рецепт.reverse=Trueпереворачивает порядок — от большего к меньшему.
Практика
Задания идут от простого к сложному. Обязательно запускайте код — понимание приходит от запуска, а не от чтения.
1. Средний балл
Создайте список из шести оценок и выведите: количество оценок, максимальную, минимальную и средний балл, округлённый до одного знака.
Подсказка: понадобятся len, max, min и sum. Для округления есть функция round(число, 1).
2. Список задач
Заведите список из трёх дел. Добавьте одно в конец, одно вставьте в начало, удалите второе по счёту. После каждого действия печатайте список с номерами через enumerate — так, чтобы человеку показывались номера с 1, а не с 0.
Подсказка: если i — индекс из enumerate, то человеку нужно показать i + 1.
3. Телефонная книга
Сделайте словарь «имя → телефон» с тремя записями. Затем в цикле спрашивайте у пользователя имя и печатайте телефон, а если такого имени нет — «Контакт не найден». Выход из цикла — по слову стоп.
Подсказка: while True с break внутри; для безопасного чтения используйте get и проверьте результат на None.
4. Общие и уникальные интересы
Задайте два множества увлечений — ваших и вашего друга. Выведите: что вас объединяет, что есть только у вас и полный список всех увлечений вдвоём. Каждый вывод сортируйте, чтобы результат был стабильным.
Подсказка: операторы &, - и |, а результат оборачивайте в sorted(...).
5. Учёт расходов за неделю
Пользователь вводит строки вида еда 500, каждую с новой строки, пока не введёт пустую. Соберите словарь «категория → сумма», где траты по одной категории складываются. В конце выведите категории, отсортированные по убыванию суммы, и общий итог.
Подсказка: разбить строку поможет split() — он вернёт список из двух элементов, второй нужно превратить в число через int(). Для накопления — counts.get(key, 0) + сумма, для сортировки — приём с key=lambda pair: pair[1] из примера выше. Что произойдёт, если пользователь введёт строку без числа? Пока можно не обрабатывать — этим займёмся в статье об ошибках и отладке.
Мини-итог
- Список
[...]— упорядоченный изменяемый набор. Доступ по индексу, нумерация с нуля, отрицательные индексы считают с конца. - В срезе
[1:4]правая граница не входит. Отсюда удобное свойство:[:n]и[n:]делят список без потерь и пересечений. append,insert,remove,sortменяют список на месте и возвращаютNone. Не присваивайте их результат.- Кортеж
(...)— то же, что список, но неизменяемый. Нужен как гарантия и как ключ словаря. - Словарь
{ключ: значение}— доступ по имени, а не по номеру. Ключи уникальны и неизменяемы, перебор пар — через.items().KeyErrorозначает «такого ключа нет»; если ключ может отсутствовать —get(ключ, по_умолчанию). - Множество
{...}— только уникальные значения, порядка нет. Быстрая проверкаin, операции&,|,-. Пустое — толькоset(). - Словарь и множество ищут за O(1) благодаря хеш-функции, список — за O(n), просматривая элементы подряд.
- Присваивание
b = aне копирует список: это вторая наклейка на те же данные. Копия —a.copy(). - Не изменяйте список, пока перебираете его циклом. Собирайте новый.
Что почитать дальше: главу «Data Structures» официального учебника Python — там же описаны списковые включения (list comprehensions), к которым стоит вернуться, когда освоитесь. И главы 4–5 книги Automate the Boring Stuff with Python Эла Свейгарта — она бесплатно доступна онлайн и разбирает списки и словари на бытовых задачах. Если захочется понять устройство коллекций глубже, это тема отдельного курса «Структуры данных».
Что дальше
Теперь программа умеет держать в памяти сколько угодно данных. Но пока всё исчезает, как только программа завершается. Следующий шаг — научиться сохранять данные в файлы и читать их обратно: обычный текст, таблицы CSV и формат JSON, на котором держится обмен данными в интернете.