Python: от данных до своей сводки Урок 15 из 56
Множества: `&`, `-`, `|` и то, чего нет во второй выгрузке
Пятнадцатый урок курса по Python. Из пяти названий в списке разных — четыре: множество съедает повтор молча. Зато три вопроса к двум выгрузкам решаются тремя знаками: `&` — что в обеих, `-` — чего не стало, `|` — всё вместе. Плюс `{}` — это пустой словарь, а не пустое множество.
Зачем это нужно
Списки и словари отвечают на вопросы «сколько» и «что по этому ключу». Есть третий вопрос, который в работе с данными задают чаще всех: есть или нет.
Пришли две выгрузки — за январь и за февраль. Что было в обеих? Чего не стало во второй? Что появилось нового? Списками это считают вложенными циклами, и в них легко ошибиться.
Множество — это набор без повторов и без порядка. Ровно то, что нужно для «есть или нет», и три вопроса к двум выгрузкам оно решает тремя знаками.
Сразу целиком
Файл jiyn.py. Запуск: python jiyn.py из окружения.
Обязательное — первые два блока: превращение списка в множество и три операции. Третий и четвёртый показывают проверку на месте и два подвоха.
"""Урок 15: множество — про «есть или нет», а не про «сколько и в каком порядке».
Две выгрузки товаров за разные месяцы. Вопросы к ним одинаковые: что есть в
обеих, чего не стало во второй, что появилось нового. Списками это считают
циклами, множествами — одним знаком.
"""
january = ["хлеб", "молоко", "масло", "соль", "хлеб"]
february = ["хлеб", "молоко", "сахар", "яйца"]
print("== что теряется при превращении в множество")
a = set(january)
b = set(february)
print("в списке января:", len(january), "| разных названий:", len(a))
print("множество печатают отсортированным:", sorted(a))
print()
print("== три вопроса — три знака")
print("есть в обеих (a & b): ", sorted(a & b))
print("пропало во второй (a - b): ", sorted(a - b))
print("появилось (b - a): ", sorted(b - a))
print("всё вместе (a | b): ", sorted(a | b))
print("только в одной (a ^ b): ", sorted(a ^ b))
print()
print("== есть или нет, и вложенность")
print("«соль» в январе:", "соль" in a, "| в феврале:", "соль" in b)
print("февраль внутри января:", b <= a, "| пересекаются:", not a.isdisjoint(b))
print("сколько всего разных за два месяца:", len(a | b))
print()
print("== два подвоха")
print("тип {} —", type({}).__name__, "| тип set() —", type(set()).__name__)
try:
{["хлеб", "молоко"]}
except TypeError as error:
print("список внутрь не положить:", error)
Выводит:
== что теряется при превращении в множество
в списке января: 5 | разных названий: 4
множество печатают отсортированным: ['масло', 'молоко', 'соль', 'хлеб']
== три вопроса — три знака
есть в обеих (a & b): ['молоко', 'хлеб']
пропало во второй (a - b): ['масло', 'соль']
появилось (b - a): ['сахар', 'яйца']
всё вместе (a | b): ['масло', 'молоко', 'сахар', 'соль', 'хлеб', 'яйца']
только в одной (a ^ b): ['масло', 'сахар', 'соль', 'яйца']
== есть или нет, и вложенность
«соль» в январе: True | в феврале: False
февраль внутри января: False | пересекаются: True
сколько всего разных за два месяца: 6
== два подвоха
тип {} — dict | тип set() — set
список внутрь не положить: cannot use 'list' as a set element (unhashable type: 'list')
Разбор
Повторы исчезают молча
в списке января: 5 | разных названий: 4
set(january) выбрасывает второй «хлеб», не сказав об этом. Это и есть главное свойство множества: элемент либо есть, либо нет, а сколько раз он встретился — вопрос не к нему.
Отсюда самое частое честное применение: посчитать, сколько разных значений в данных. len(set(...)) против len(...) — одна строка, и сразу видно, есть ли в выгрузке дубликаты.
И отсюда же ошибка: множество нельзя брать там, где повторы — это данные. Три покупки хлеба за месяц — это три покупки, а не одна; для них нужен список или счётчик, а не множество.
Образ. Список приглашённых. В нём важно, кто приглашён, а не сколько раз его вписали. Но если это список покупок, то две буханки — это две буханки.
Порядка нет, поэтому печатают sorted
print("множество печатают отсортированным:", sorted(a))
У множества нет порядка — ни того, в котором добавляли, ни какого-либо другого. Напечатайте его напрямую, и порядок может оказаться другим на другой машине или при другом запуске: он зависит от того, как элементы разложились внутри.
Поэтому правило простое: множество показывают через sorted. Тогда вывод одинаков у всех, и его можно сравнивать глазами. Внутри программы порядок не нужен — там множество спрашивают, а не читают.
Три вопроса — три знака
есть в обеих (a & b): ['молоко', 'хлеб']
пропало во второй (a - b): ['масло', 'соль']
появилось (b - a): ['сахар', 'яйца']
& — пересечение: что есть и там, и там. - — разность: что есть в первом и нет во втором. | — объединение: всё вместе, по одному разу. ^ — симметричная разность: то, что есть ровно в одном из двух.
Порядок в - важен: a - b и b - a отвечают на разные вопросы — «чего не стало» и «что появилось». Это самая частая путаница на первых порах, и лечится она вопросом вслух: из какого множества вычитаем.
У всех знаков есть словесные двойники: a.intersection(b), a.difference(b), a.union(b), a.symmetric_difference(b). Знаки короче, слова понятнее в чужом коде — берут то, что читается на месте.
«Есть или нет» — то, ради чего множества и придуманы
"соль" in a
Проверка in работает и со списком, и с множеством, но по-разному. В списке Python идёт по элементам, пока не найдёт: чем длиннее список, тем дольше. В множестве он сразу считает, где элемент должен лежать, и смотрит только туда — длина не важна.
На десяти строках разницы не заметно. На выгрузке в сто тысяч строк, которую проверяют в цикле, это разница между «секунда» и «полчаса» — и это единственная причина, по которой множество иногда заводят просто ради проверок.
Рядом стоят вопросы о целых наборах: b <= a — все ли элементы b есть в a; a.isdisjoint(b) — не пересекаются ли они вовсе.
Два подвоха
тип {} — dict | тип set() — set
Фигурные скобки заняты словарём: {} — это пустой словарь, а не пустое множество. Пустое множество — только set(). Ошибку видно не сразу: seen = {} работает до первой строки seen.add(...), и там уже говорит, что у словаря нет add.
список внутрь не положить: cannot use 'list' as a set element (unhashable type: 'list')
В множество можно класть только то, что нельзя изменить: числа, строки, кортежи. Список изменяем — а значит, положив его внутрь, вы могли бы поменять элемент, уже разложенный по местам, и множество перестало бы находить сам себя.
Причина та же, по которой ключом словаря не бывает список: и там, и там элемент раскладывают по хешу. Если нужен неизменяемый набор — есть frozenset, и его как раз можно положить внутрь другого множества.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Что множество делает с повторами и когда это вредно?
- Чем
a - bотличается отb - aна двух выгрузках? - Почему множество печатают через
sorted?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта строка?
print(len([1, 2, 2, 3]), len({1, 2, 2, 3}))
2. Заполните пропуск. Вместо ... поставьте то, что оставит названия, которых нет во второй выгрузке.
a = {"хлеб", "молоко", "соль"}
b = {"хлеб", "молоко", "сахар"}
print(sorted(...))
3. Почините. Программа падает на второй строке. Прочитайте ошибку и заведите то, что действительно нужно.
seen = {}
seen.add("хлеб")
print(sorted(seen))
Задание
Обязательное. Дано:
january = ["хлеб", "молоко", "масло", "соль", "хлеб", "яйца"]
february = ["хлеб", "молоко", "сахар", "яйца", "яйца"]
Напечатайте по каждому месяцу, сколько было позиций и сколько среди них разных. Затем — три ответа: чего не стало во втором месяце, что появилось нового, что осталось в обоих. В конце — сколько всего разных названий за два месяца. Все наборы печатайте через sorted.
Ожидаемый вывод:
январь: позиций 6 | разных 5
февраль: позиций 5 | разных 4
пропало во втором: ['масло', 'соль']
появилось во втором: ['сахар']
осталось в обоих: ['молоко', 'хлеб', 'яйца']
всего разных за два месяца: 6
Готово, когда: вывод совпадает построчно; ни одного вложенного цикла — все три ответа получены знаками; каждое множество напечатано через sorted, а не напрямую.
На своих данных. Возьмите два своих списка — товары из двух чеков, участники двух чатов, файлы в двух папках. Ответьте на те же три вопроса и скажите вслух, какой из них понадобился бы вам в жизни чаще.
По желанию.
- Напечатайте множество без
sortedнесколько раз подряд и посмотрите на порядок. - Соберите
frozensetиз первого месяца и положите его внутрь другого множества. - Посчитайте, сколько раз встречается каждое название, — и объясните, почему тут множество не годится.
Куда это встанет в проекте
Сводка начинает замечать пропажи. Раньше она считала то, что пришло; теперь может сказать, чего в новой выгрузке нет по сравнению с прошлой — а это первый признак того, что сломался источник, а не изменился мир.
Долги. Множество отвечает «есть или нет», но не «сколько раз» — для этого нужен счётчик, и он появится в уроке про модуль collections. И порядок оно не хранит, поэтому «первые пять новых» из него без сортировки не достать.
Ответы
Показать ответы
На вопросы
- Выбрасывает их молча: элемент либо есть, либо нет. Вредно это там, где повтор — данные: три покупки хлеба превратятся в одну, и сумма чека сойдётся неверно.
a - b— чего не стало во второй выгрузке,b - a— что в ней появилось. Разные вопросы, и порядок вычитания решает, на какой вы отвечаете.- Потому что своего порядка у множества нет: прямой вывод может оказаться другим на другой машине или в другом запуске.
sortedделает вывод одинаковым для всех.
К разминке
4 3. В списке четыре элемента, в множестве — три: второй двойки в нём нет, и никто об этом не сообщил.
4 3
a - b. Разность в этом порядке оставляет то, что есть в первом наборе и чего нет во втором.
a = {"хлеб", "молоко", "соль"}
b = {"хлеб", "молоко", "сахар"}
print(sorted(a - b))
['соль']
{}— это пустой словарь, а не пустое множество, поэтомуaddу него не нашлось:AttributeError: 'dict' object has no attribute 'add'. Пустое множество заводят черезset().
seen = set()
seen.add("хлеб")
print(sorted(seen))
['хлеб']
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.