Глава 6. Множества и кортежи: поиск дублей и защита неизменяемых записей
Задача главы: научиться находить дубли и расхождения между двумя выгрузками через множества и хранить неизменяемые записи-строки журнала в кортежах.
6.1 Поиск дублей: set(), сравнение len(list) и len(set(list))
Зачем это нужно. Список из главы Списки хранит элементы в том порядке, в каком они пришли, и без вопросов допускает повторы. Когда нужно быстро понять, есть ли в выгрузке задвоенные строки, — на помощь приходит множество: коллекция уникальных элементов без порядка.
Сравнение len(list) и len(set(list)) — это самый быстрый способ узнать сам факт наличия дублей, не выписывая вручную, какие именно фамилии повторились.
⚠️ Подводный камень
Множество не просто убирает повторы — оно ещё и отказывается от порядка элементов и от доступа по позиции. Исходный список им подменять нельзя.
Traceback (most recent call last): File "report.py", line 4, in <module> first_in_registry = unique_names[0]TypeError: 'set' object is not subscriptable| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
TypeError | «У множества нет индексов» | Множество не хранит порядок элементов, поэтому обращение по позиции [0] для него не определено в принципе. Если порядок важен, множество для этой роли не подходит. |
Правильный паттерн
Пояснение логики: dict.fromkeys(list) создаёт словарь, где элементы списка стали ключами, — а ключи в Python начиная с версии 3.7 хранятся в порядке добавления и не повторяются. Обернув результат в list(...), получаем список без дублей, но с сохранённым исходным порядком. Правило простое: set() годится для подсчёта и проверки на совпадение, но как только нужен порядок или доступ по индексу — за дело берётся список.
6.2 Операции над множествами: -, &, | для сверки двух выгрузок
Зачем это нужно. Классическая задача аналитика — сравнить список учеников за два периода: кто выбыл, кто появился новый, кто остался в обеих выгрузках. Перебирать оба списка вручную циклами долго и ошибкоёмко — операции над множествами делают это одной строкой.
- (разность), & (пересечение) и | (объединение) в точности повторяют школьные операции над множествами — с той разницей, что порядок операндов важен для разности: A - B и B - A — это разные ответы на разные вопросы.
⚠️ Подводный камень
Traceback тут не появится — программа отработает и выдаст результат. Но если две выгрузки пришли из разных источников (например, из разных систем учёта), ФИО в них могут отличаться регистром или пробелами, а множество сравнивает элементы буквально, без снисхождения.
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
| — (тихая ошибка) | «'Иванов Иван' и 'иванов иван' — два разных элемента» | Множество сравнивает строки посимвольно и не знает, что это один и тот же ученик. Регистр и лишние пробелы нужно привести к общему виду до сравнения — так же, как в главе Строки и f-строки. |
Правильный паттерн
Пояснение логики: .add(item) — аналог .append() у списка, только для множества и только для одного элемента за раз. Нормализация .strip().lower() перед сравнением делает выгрузки сопоставимыми: теперь 'Иванов Иван' и 'иванов иван' — один и тот же элемент множества, а не два разных. Правило на будущее: прежде чем сравнивать текстовые данные из разных источников, привести их к единому регистру и обрезать пробелы — иначе результат сверки будет формально правильным, но по сути враньём.
6.3 Кортежи как неизменяемая запись, распаковка name, score = row
Зачем это нужно. Строка журнала — «ученик, группа, балл» — это фиксированная структура: количество и порядок полей не должны меняться на ходу. Кортеж — это список, который сознательно нельзя менять после создания, что как раз годится для такой записи.
Распаковка name, group, score = student_record работает только тогда, когда количество переменных слева совпадает с количеством элементов в кортеже справа — Python сопоставляет их строго по позиции.
⚠️ Подводный камень
Неизменяемость — это не рекомендация, а жёсткое ограничение: попытка поменять элемент кортежа на месте ломает программу.
Traceback (most recent call last): File "report.py", line 3, in <module> student_record[2] = 95TypeError: 'tuple' object does not support item assignment| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
TypeError | «Кортеж не поддерживает присваивание по индексу» | Кортеж специально не даёт менять свои элементы — это гарантия для читающего код, что запись останется такой, какой была создана. Нужен новый балл — нужен новый кортеж, а не правка старого. |
Похожая по формулировке, но другая по причине ошибка возникает при распаковке, если число переменных не совпадает с числом полей:
Traceback (most recent call last): File "report.py", line 3, in <module> name, score = student_recordValueError: too many values to unpack (expected 2)| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
ValueError | «Элементов для распаковки больше, чем переменных слева» | Схема записи изменилась (добавилось поле group), а код распаковки — нет. Число переменных слева должно точно совпадать с числом полей в кортеже. |
Правильный паттерн
Пояснение логики: student_record[:2] — срез, знакомый по главам Строки и f-строки и Списки, берёт первые два поля как есть, а (95,) — кортеж из одного элемента (запятая внутри скобок обязательна, иначе это просто число в скобках). Сложение + двух кортежей даёт третий, новый, — старый student_record при этом остаётся нетронутым. Если данные по своей природе должны меняться часто (баллы за модуль курса, накопление списка), для них с самого начала стоит выбирать список, а не кортеж; кортеж — осознанный выбор именно для того, что меняться не должно.
6.4 Мини-словарик ошибок шестой главы
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
TypeError (индекс множества) | «У множества нет индексов» | Множество не хранит порядок — доступ по позиции [0] для него не определён. Нужен порядок — нужен список (dict.fromkeys() для дедупликации с сохранением порядка). |
TypeError (изменение кортежа) | «Кортеж не поддерживает присваивание по индексу» | Кортеж неизменяем по конструкции — новое значение поля означает новый кортеж, а не правку старого. |
ValueError (распаковка) | «Число значений для распаковки не совпадает с числом переменных» | Схема записи разошлась с кодом распаковки — добавилось или пропало поле. Число переменных слева должно точно совпадать с числом элементов кортежа. |
| — (тихая ошибка) | «Строки, различающиеся регистром или пробелами, — разные элементы множества» | Сверка двух выгрузок с разным форматом ФИО молча даёт неверный список «пропавших»/«новых». Нормализация (.strip().lower()) обязательна до сравнения множеств. |
Мини-кейс: сверка двух выгрузок — дубли, пропавшие и новые ученики
Задача: по спискам ФИО за прошлый и текущий месяц найти задвоенные записи внутри каждой выгрузки, а также тех, кто выбыл, кто появился новый и кто остался, — с учётом того, что источники пишут ФИО в разном регистре.
--- Сверка выгрузок ---Дубли в прошлой выгрузке: TrueДубли в текущей выгрузке: TrueВыбыло: 1 — {'иванов иван'}Новых: 1 — {'петров пётр'}Осталось: 2 — {'смирнова ольга', 'ким юрий'}Что получилось: проверка на дубли выполнена по сырым спискам до нормализации — иначе сам факт задвоения потерялся бы вместе с приведением регистра. Сверка между выгрузками, наоборот, потребовала нормализации: без .strip().lower() 'Смирнова Ольга' и 'смирнова ольга' числились бы как два разных ученика, и отчёт лживо показал бы одного «выбывшего» и одного «нового» вместо одного и того же человека. Три счётчика на выходе собраны в кортеж и сразу распакованы — это подчёркивает, что схема сводки («сколько выбыло, сколько новых, сколько осталось») фиксирована и не должна молча обрасти четвёртым полем без изменения кода вокруг. ⚠️ Дальнейшая работа с именами реальных учеников — не по множеству, а по упорядоченному списку с сохранением исходного регистра для печати в отчёте — потребует dict.fromkeys() из 6.1, а не прямого перебора множества.
Чек-лист главы
- Использую
set()и сравнениеlen(list)/len(set(list)), чтобы быстро проверить выгрузку на дубли - Не обращаюсь к множеству по индексу и не полагаюсь на его порядок — для этого беру список
- Убираю дубли с сохранением порядка через
list(dict.fromkeys(list)), а не черезset() - Свожу две выгрузки операциями
-(разность),&(пересечение),|(объединение), а не ручными циклами - Нормализую регистр и пробелы (
.strip().lower()) перед сравнением множеств из разных источников - Храню неизменяемую запись строки журнала в кортеже, а не в списке
- Распаковываю кортеж, только когда число переменных слева точно совпадает с числом полей справа
- Помню, что кортеж нельзя менять на месте — новое значение поля означает пересборку нового кортежа
- Узнаю
TypeError: 'tuple' object does not support item assignmentкак сигнал «кортеж, а не список» - Узнаю
ValueError: too many/not enough values to unpackкак сигнал «схема записи разошлась с кодом распаковки»
Дальше: Условия