Shanraq.org Shanraq.org
Множества: `&`, `-`, `|` и то, чего нет во второй выгрузке
IT

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, и его как раз можно положить внутрь другого множества.

Карта урока

Карта урока: повторы, три знака и «есть или нет»

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Что множество делает с повторами и когда это вредно?
  2. Чем a - b отличается от b - a на двух выгрузках?
  3. Почему множество печатают через 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. И порядок оно не хранит, поэтому «первые пять новых» из него без сортировки не достать.

Ответы

Показать ответы

На вопросы

  1. Выбрасывает их молча: элемент либо есть, либо нет. Вредно это там, где повтор — данные: три покупки хлеба превратятся в одну, и сумма чека сойдётся неверно.
  2. a - b — чего не стало во второй выгрузке, b - a — что в ней появилось. Разные вопросы, и порядок вычитания решает, на какой вы отвечаете.
  3. Потому что своего порядка у множества нет: прямой вывод может оказаться другим на другой машине или в другом запуске. sorted делает вывод одинаковым для всех.

К разминке

  1. 4 3. В списке четыре элемента, в множестве — три: второй двойки в нём нет, и никто об этом не сообщил.
4 3
  1. a - b. Разность в этом порядке оставляет то, что есть в первом наборе и чего нет во втором.
a = {"хлеб", "молоко", "соль"}
b = {"хлеб", "молоко", "сахар"}
print(sorted(a - b))
['соль']
  1. {} — это пустой словарь, а не пустое множество, поэтому add у него не нашлось: AttributeError: 'dict' object has no attribute 'add'. Пустое множество заводят через set().
seen = set()
seen.add("хлеб")
print(sorted(seen))
['хлеб']

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.