Skip to content

Глава 6. Множества и кортежи: поиск дублей и защита неизменяемых записей

Задача главы: научиться находить дубли и расхождения между двумя выгрузками через множества и хранить неизменяемые записи-строки журнала в кортежах.


6.1 Поиск дублей: set(), сравнение len(list) и len(set(list))

Зачем это нужно. Список из главы Списки хранит элементы в том порядке, в каком они пришли, и без вопросов допускает повторы. Когда нужно быстро понять, есть ли в выгрузке задвоенные строки, — на помощь приходит множество: коллекция уникальных элементов без порядка.

// PYTHON_EXECUTOR (Pyodide Wasm)

Сравнение len(list) и len(set(list)) — это самый быстрый способ узнать сам факт наличия дублей, не выписывая вручную, какие именно фамилии повторились.

⚠️ Подводный камень

Множество не просто убирает повторы — оно ещё и отказывается от порядка элементов и от доступа по позиции. Исходный список им подменять нельзя.

// PYTHON_EXECUTOR (Pyodide Wasm)
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] для него не определено в принципе. Если порядок важен, множество для этой роли не подходит.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: dict.fromkeys(list) создаёт словарь, где элементы списка стали ключами, — а ключи в Python начиная с версии 3.7 хранятся в порядке добавления и не повторяются. Обернув результат в list(...), получаем список без дублей, но с сохранённым исходным порядком. Правило простое: set() годится для подсчёта и проверки на совпадение, но как только нужен порядок или доступ по индексу — за дело берётся список.


6.2 Операции над множествами: -, &, | для сверки двух выгрузок

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

// PYTHON_EXECUTOR (Pyodide Wasm)

- (разность), & (пересечение) и | (объединение) в точности повторяют школьные операции над множествами — с той разницей, что порядок операндов важен для разности: A - B и B - A — это разные ответы на разные вопросы.

⚠️ Подводный камень

Traceback тут не появится — программа отработает и выдаст результат. Но если две выгрузки пришли из разных источников (например, из разных систем учёта), ФИО в них могут отличаться регистром или пробелами, а множество сравнивает элементы буквально, без снисхождения.

// PYTHON_EXECUTOR (Pyodide Wasm)
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«'Иванов Иван' и 'иванов иван' — два разных элемента»Множество сравнивает строки посимвольно и не знает, что это один и тот же ученик. Регистр и лишние пробелы нужно привести к общему виду до сравнения — так же, как в главе Строки и f-строки.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: .add(item) — аналог .append() у списка, только для множества и только для одного элемента за раз. Нормализация .strip().lower() перед сравнением делает выгрузки сопоставимыми: теперь 'Иванов Иван' и 'иванов иван' — один и тот же элемент множества, а не два разных. Правило на будущее: прежде чем сравнивать текстовые данные из разных источников, привести их к единому регистру и обрезать пробелы — иначе результат сверки будет формально правильным, но по сути враньём.


6.3 Кортежи как неизменяемая запись, распаковка name, score = row

Зачем это нужно. Строка журнала — «ученик, группа, балл» — это фиксированная структура: количество и порядок полей не должны меняться на ходу. Кортеж — это список, который сознательно нельзя менять после создания, что как раз годится для такой записи.

// PYTHON_EXECUTOR (Pyodide Wasm)

Распаковка name, group, score = student_record работает только тогда, когда количество переменных слева совпадает с количеством элементов в кортеже справа — Python сопоставляет их строго по позиции.

⚠️ Подводный камень

Неизменяемость — это не рекомендация, а жёсткое ограничение: попытка поменять элемент кортежа на месте ломает программу.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 3, in <module>
student_record[2] = 95
TypeError: 'tuple' object does not support item assignment
ОшибкаЧто значит для PythonЧеловеческий перевод
TypeError«Кортеж не поддерживает присваивание по индексу»Кортеж специально не даёт менять свои элементы — это гарантия для читающего код, что запись останется такой, какой была создана. Нужен новый балл — нужен новый кортеж, а не правка старого.

Похожая по формулировке, но другая по причине ошибка возникает при распаковке, если число переменных не совпадает с числом полей:

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 3, in <module>
name, score = student_record
ValueError: too many values to unpack (expected 2)
ОшибкаЧто значит для PythonЧеловеческий перевод
ValueError«Элементов для распаковки больше, чем переменных слева»Схема записи изменилась (добавилось поле group), а код распаковки — нет. Число переменных слева должно точно совпадать с числом полей в кортеже.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: student_record[:2] — срез, знакомый по главам Строки и f-строки и Списки, берёт первые два поля как есть, а (95,) — кортеж из одного элемента (запятая внутри скобок обязательна, иначе это просто число в скобках). Сложение + двух кортежей даёт третий, новый, — старый student_record при этом остаётся нетронутым. Если данные по своей природе должны меняться часто (баллы за модуль курса, накопление списка), для них с самого начала стоит выбирать список, а не кортеж; кортеж — осознанный выбор именно для того, что меняться не должно.


6.4 Мини-словарик ошибок шестой главы

ОшибкаЧто значит для PythonЧеловеческий перевод
TypeError (индекс множества)«У множества нет индексов»Множество не хранит порядок — доступ по позиции [0] для него не определён. Нужен порядок — нужен список (dict.fromkeys() для дедупликации с сохранением порядка).
TypeError (изменение кортежа)«Кортеж не поддерживает присваивание по индексу»Кортеж неизменяем по конструкции — новое значение поля означает новый кортеж, а не правку старого.
ValueError (распаковка)«Число значений для распаковки не совпадает с числом переменных»Схема записи разошлась с кодом распаковки — добавилось или пропало поле. Число переменных слева должно точно совпадать с числом элементов кортежа.
— (тихая ошибка)«Строки, различающиеся регистром или пробелами, — разные элементы множества»Сверка двух выгрузок с разным форматом ФИО молча даёт неверный список «пропавших»/«новых». Нормализация (.strip().lower()) обязательна до сравнения множеств.

Мини-кейс: сверка двух выгрузок — дубли, пропавшие и новые ученики

Задача: по спискам ФИО за прошлый и текущий месяц найти задвоенные записи внутри каждой выгрузки, а также тех, кто выбыл, кто появился новый и кто остался, — с учётом того, что источники пишут ФИО в разном регистре.

// PYTHON_EXECUTOR (Pyodide Wasm)
--- Сверка выгрузок ---
Дубли в прошлой выгрузке: 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 как сигнал «схема записи разошлась с кодом распаковки»

Дальше: Условия