Модуль 4 · Словари, множества и кортежи

Справочник по Python · модуль 4 · словари, множества и кортежи

Глава 4.7

4.7

Операции над
множествами

Объединение, пересечение и две разности отвечают на вопросы о двух наборах сразу — без единого цикла. Это главное, ради чего множества стоит выбирать: задачи вида «чего не хватает» и «что общего» решаются одной строкой.

Категория
Операции set
Уровень
Базовый → средний
Связанные темы
Множества, логика, сравнение наборов
Зачем это знать
Сравнивать наборы данных без циклов

§1Вопросы о двух наборах

Пусть у пользователя есть набор прав, а страница требует своего набора. Вопросы возникают одни и те же.

granted→{"read", "write"}
required→{"write", "delete"}
ВопросОперация
что есть в обоих наборахпересечение &
чего не хватаетразность -
что есть хоть где-тообъединение |
что различаетсясимметрическая разность ^
хватает ли прав целикомподмножество <=

Каждый из этих вопросов можно решить циклом с проверками, но операция короче, быстрее и читается как формулировка задачи.

Схемачетыре операции на одной паре наборов
объединение a | b a b пересечение a & b a b разность a - b a b симметрическая разность a ^ b a b
  • элементы, попадающие в результат
  • элементы, которые операция отбрасывает

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

§2Объединение

Объединение собирает всё, что есть хотя бы в одном из наборов, — повторы при этом исчезают сами.

main.pyкакие технологии нужны команде
backend = {"Python", "SQL"}
frontend = {"JavaScript", "HTML", "SQL"}

print(sorted(backend | frontend))
ВыводSQL был в обоих, но остался один
['HTML', 'JavaScript', 'Python', 'SQL']
Схемачто попадает в результат
A | B     только в A  +  общие  +  только в B

§3Пересечение

Пересечение оставляет только то, что есть в обоих наборах.

main.pyкому отправлять рассылку
active = {101, 102, 103}
subscribed = {102, 103, 104}

print(sorted(active & subscribed))
Выводактивные и подписанные одновременно
[102, 103]

Символ & выбран не случайно: он же означает логическое «и» в побитовых операциях. Пересечение — это «элемент в A и в B», объединение — «в A или в B».

§4Разность

Разность оставляет элементы левого набора, которых нет в правом.

main.pyкаких прав не хватает
required = {"read", "write", "delete"}
granted = {"read", "write"}

print(sorted(required - granted))
Выводне выдано одно право
['delete']

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

§5Направление разности

В отличие от объединения и пересечения, разность несимметрична: порядок операндов меняет смысл.

main.pyдва разных вопроса
required = {"read", "write", "delete"}
granted = {"read", "write"}

print(required - granted)
print(granted - required)
Выводнедостающее и лишнее
{'delete'}
set()

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

§6Симметрическая разность

Эта операция оставляет то, что принадлежит ровно одному из наборов, — то есть всё, кроме общего.

main.pyчто изменилось за сутки
yesterday = {"A", "B", "C"}
today = {"B", "C", "D"}

print(sorted(yesterday ^ today))
ВыводA исчезло, D появилось
['A', 'D']

Результат отвечает «что различается», но не говорит, в какую сторону: A и D здесь равноправны. Если направление важно, берут две обычные разности.

Схемачетыре операции на одних данных
A = {A, B, C}      B = {B, C, D}

A | B  →  A B C D     всё
A & B  →    B C       общее
A - B  →  A           только слева
A ^ B  →  A     D     только с одной стороны

§7Методы и операторы

У каждой операции есть два вида записи.

ОперацияМетодОператор
объединениеa.union(b)a | b
пересечениеa.intersection(b)a & b
разностьa.difference(b)a - b
симметрическая разностьa.symmetric_difference(b)a ^ b

Результат одинаковый — выбирают по читаемости. Операторы короче, методы понятнее тем, кто видит код впервые.

§8Разница между ними

Одно отличие всё-таки есть, и оно практическое: метод принимает любую итерируемую коллекцию, оператор — только множество.

main.pyсправа обычный список
print({1, 2} | [3])
Ошибкаоператор строг к типам
TypeError: unsupported operand type(s) for |: 'set' and 'list'
main.pyметод список принимает
print(sorted({1, 2}.union([3])))
Выводпреобразование произошло само
[1, 2, 3]

Отсюда правило: если один из наборов — список, либо преобразуйте его в множество, либо используйте метод.

§9Подмножество

Проверка «все ли элементы A входят в B» отвечает на вопрос вида «хватает ли прав».

main.pyтребуемое входит в выданное
required = {"read", "write"}
granted = {"read", "write", "delete"}

print(required.issubset(granted))
print(required <= granted)
Выводправ достаточно
True
True

Запись required <= granted читается как «требуемое не больше выданного». Это гораздо короче, чем цикл с проверкой каждого права.

§10Надмножество

Зеркальная проверка — «содержит ли B всё из A».

main.pyтот же вопрос с другой стороны
required = {"read", "write"}
granted = {"read", "write", "delete"}

print(granted.issuperset(required))
print(granted >= required)
Выводто же самое, что в §9
True
True

Выбор между issubset() и issuperset() — вопрос того, что удобнее поставить в начало фразы.

§11Строгие и нестрогие проверки

Знаки < и <= различаются так же, как у чисел: строгий вариант требует, чтобы множества не были равны.

main.pyсравниваем множество с самим собой
values = {"read", "write"}

print(values <= values)
print(values < values)
Выводравное множество не является строгим подмножеством
True
False
ЗаписьЗначениеРавные множества
a <= b, a.issubset(b)A входит в BTrue
a < bA входит в B и меньше негоFalse
a >= b, a.issuperset(b)A содержит BTrue
a == bнаборы совпадаютTrue

В проверках прав почти всегда нужен нестрогий вариант: совпадение наборов — нормальная ситуация, а не отказ.

§12isdisjoint(): нет общего

Метод отвечает, что пересечение пусто, — и делает это, не строя само пересечение.

main.pyдва набора без общих элементов
print({1, 2}.isdisjoint({3, 4}))
print({1, 2}.isdisjoint({2, 3}))
Выводво втором случае двойка общая
True
False

То же самое можно записать как not (a & b), но isdisjoint() останавливается на первом общем элементе и не создаёт промежуточное множество.

§13Несколько наборов сразу

Операции можно объединять в цепочку — или передать методу несколько аргументов.

main.pyчто есть во всех трёх наборах
a = {1, 2, 3}
b = {2, 3, 4}
c = {3, 4, 5}

print(sorted(a & b & c))
print(sorted(a.intersection(b, c)))
Выводобщая только тройка
[3]
[3]

Метод с несколькими аргументами удобнее, когда наборы лежат в списке: тогда их распаковывают звёздочкой из главы 6.6 — set.union(*groups).

§14Практика: права доступа

Собрать всё вместе на одной задаче.

main.pyтри вопроса об одних данных
granted = {"read", "write"}
required = {"write", "delete"}

print(required <= granted)
print(sorted(required - granted))
print(sorted(required & granted))
Выводправ не хватает, и видно каких
False
['delete']
['write']

Первая строка отвечает «пускать или нет», вторая — «что именно запросить у администратора», третья — «что уже есть». Три цикла с условиями заменены тремя выражениями, каждое из которых читается как вопрос.

§15Частые ошибки

Что написаноЧто произойдётКак правильно
granted - required вместо required - grantedответ на другой вопрос, без ошибкипомнить: слева то, из чего вычитаем
{1, 2} | [3]TypeError: оператор требует множество{1, 2}.union([3])
a ^ b для «что добавилось»вернётся и добавленное, и исчезнувшееb - a
a < b для проверки правравные наборы дадут Falsea <= b
ждать, что a - b изменит aоператоры создают новое множествометоды с _update из главы 4.8
a and b вместо a & bвернётся одно из множеств целикомдля пересечения нужен &
сравнивать множества оператором > как размерыэто проверка надмножества, а не длиныlen(a) > len(b)

§16Проверьте себя

Какая операция несимметрична?

Разность: a - b и b - a отвечают на разные вопросы. Объединение, пересечение и симметрическая разность от порядка не зависят.

Чем a ^ b отличается от a - b?

Симметрическая разность даёт всё, что есть ровно в одном наборе, — с обеих сторон. Обычная разность — только то, что есть слева и нет справа.

Почему {1, 2} | [3] не работает, а union([3]) работает?

Оператор требует, чтобы оба операнда были множествами. Метод принимает любую итерируемую коллекцию и преобразует её сам.

Чем a < b отличается от a <= b?

Строгий вариант требует, чтобы множества не были равны: values < values даёт False.

Как проверить, что у наборов нет общих элементов?

a.isdisjoint(b). Это быстрее, чем строить пересечение и проверять его на пустоту.

Как одной строкой узнать, каких прав не хватает?

required - granted: элементы требуемого набора, которых нет в выданном.

§17Шпаргалка

Всёa | b
Общееa & b
Чего нет справаa - b
Что различаетсяa ^ b
Хватает лиrequired <= granted
Нет общегоa.isdisjoint(b)
Со спискомa.union(items)
Несколько наборовa.intersection(b, c)

Итог главы

Четыре операции отвечают на вопросы о двух наборах без циклов: объединение даёт всё, пересечение — общее, разность — то, чего нет справа, симметрическая разность — то, что есть ровно с одной стороны. Разность единственная из них несимметрична, и перепутанный порядок операндов даёт правдоподобный ответ на другой вопрос. Проверки <= и >= отвечают «входит ли один набор в другой» — это готовая проверка достаточности прав. Операторы требуют множеств с обеих сторон, методы принимают любые коллекции.

Глава 4.7 · Операции над множествами · Справочник по Python Автор: Макаров Максим Николаевич