Программирование с нуля Списки, словари и множества: работа с наборами данных
0%

Списки, словари и множества: работа с наборами данных

Списки, словари и множества: работа с наборами данных

До сих пор одна переменная хранила одно значение: возраст, имя, цену. Этого хватает, пока данных мало. Но настоящие задачи выглядят иначе — не «цена товара», а «цены всех товаров в чеке». Не «оценка», а «оценки всего класса».

Хранить такое в отдельных переменных невозможно физически. Тридцать учеников — тридцать переменных 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().

Как выбрать структуру

Это решение принимается почти механически — по тому, как вы будете обращаться к данным.

Структура Скобки Порядок Повторы Изменяемая Поиск элемента
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, на котором держится обмен данными в интернете.

Файлы и форматы данных: текст, CSV, JSON

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов