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

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

Глава 4.5

4.5

Множества
в Python

Множество хранит только уникальные элементы и не связывает их ни с индексами, ни с ключами. Взамен оно отвечает на вопрос «есть ли здесь такое значение?» практически мгновенно — независимо от того, сто в нём элементов или миллион.

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

§1Зачем нужно множество

Две задачи встречаются постоянно: «убрать повторы» и «проверить, встречалось ли это раньше». Списком обе решаются, но плохо.

Сравнениепроверка принадлежности
value in big_list    Python сравнивает элементы по очереди
value in big_set     Python сразу вычисляет, где значение должно лежать

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

§2Что хранит set

Множество (set) — изменяемая коллекция уникальных хешируемых элементов без порядка и индексов.

КоллекцияГлавное свойствоДоступПовторы
listпорядокпо индексуразрешены
tupleпорядок, неизменяемостьпо индексуразрешены
dictключ → значениепо ключуключи уникальны
setуникальностьтолько inневозможны

§3Создание множества

main.pyлитерал и функция set()
languages = {"Python", "Go", "Java"}
letters = set("hello")

print(sorted(languages))
print(sorted(letters))
Выводвторая строка — уникальные буквы слова
['Go', 'Java', 'Python']
['e', 'h', 'l', 'o']

В слове hello две буквы l, но в множестве осталась одна. Вывод здесь обёрнут в sorted() намеренно — почему, объясняет §7.

§4Повторы исчезают

main.pyпять значений в литерале
numbers = {1, 2, 2, 3, 1}

print(numbers)
print(len(numbers))
Выводуникальных только три
{1, 2, 3}
3

Добавление уже существующего элемента не создаёт второй экземпляр и не вызывает ошибку — операция просто ничего не делает. Это отличает add() от append() у списка.

§5Пустое множество

Фигурные скобки заняты словарём, поэтому пустое множество создают только функцией.

main.pyдве пустые коллекции
empty_set = set()
empty_dict = {}

print(type(empty_set))
print(type(empty_dict))
Выводразные типы
<class 'set'>
<class 'dict'>

Причина историческая: словарь появился в языке раньше и занял литерал {}. Ошибка коварна тем, что не проявляется сразу — {}.add(x) упадёт лишь на первой попытке добавить элемент.

§6Индексов нет

main.pyобращение по позиции
values = {10, 20, 30}
print(values[0])
Ошибкапозиций у множества нет
TypeError: 'set' object is not subscriptable

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

§7Порядок не гарантирован

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

Ошибка, которая проходит тесты

Если код опирается на порядок вывода множества, он может работать месяцами и сломаться при первом же изменении данных. Для воспроизводимого вывода всегда используйте sorted(values).

В отличие от словаря, который с Python 3.7 сохраняет порядок добавления, множество такой гарантии не даёт вовсе.

§8Перебор множества

Перебирать множество можно — цикл посетит каждый элемент ровно один раз.

main.pyпредсказуемый порядок вывода
roles = {"reader", "editor", "admin"}

for role in sorted(roles):
    print(role)
Выводпо алфавиту
admin
editor
reader

Без sorted() результат тоже будет полным, но его порядок непредсказуем.

§9Проверка наличия

Это главная операция множества и причина его выбирать.

main.pyразрешённые расширения файлов
allowed = {"jpg", "png", "webp"}

print("png" in allowed)
print("exe" not in allowed)
Выводдве проверки
True
True

Сравните с проверкой через or из главы 5.5: ext == "jpg" or ext == "png" or ext == "webp". Множество короче, а добавление нового формата — это правка одной строки.

§10Насколько это быстрее

Разница между in для списка и для множества не косметическая. В списке Python сравнивает элементы по очереди, в множестве — сразу вычисляет, где значение должно лежать.

main.pyсто тысяч элементов, ищем последний
import timeit

big_list = list(range(100_000))
big_set = set(big_list)

print(timeit.timeit(lambda: 99_999 in big_list, number=200))
print(timeit.timeit(lambda: 99_999 in big_set, number=200))
Примерный выводточные числа зависят от машины
0.0819...
0.0000064...

Разница здесь — четыре порядка, и она растёт вместе с размером данных.

Операцияlistset
проверка inO(n)O(1)
добавлениеO(1) в конецO(1)
удаление по значениюO(n)O(1)
доступ по индексуO(1)невозможен

Отсюда практический приём: если по списку приходится часто проверять принадлежность, один раз преобразуйте его в множество — known = set(items) — и проверяйте уже по нему.

§11Элемент должен быть хешируемым

Требование то же, что к ключам словаря в главе 4.2, и по той же причине.

main.pyсписок в множестве
values = {[1, 2]}
Ошибказнакомое сообщение
TypeError: unhashable type: 'list'
Можно добавитьНельзя добавить
числа, строки, boolсписки
кортежи из неизменяемыхсловари
frozensetдругие множества

Обратите внимание на последнюю строку: множество внутри множества невозможно, потому что set изменяем. Для этого случая существует frozenset — глава 4.9.

§12Равные значения разных типов

Уникальность определяется равенством, а не типом. Значения, равные друг другу, считаются одним элементом.

main.pyтри разных литерала
print({1, 1.0, True})
Выводостался один элемент
{1}

В Python 1 == 1.0 == True, а значит для множества это одно и то же значение. Остаётся тот экземпляр, который добавили первым. То же правило действует для ключей словаря, и на практике оно всплывает при смешивании целых и дробных чисел или bool с числами.

§13Удаление повторов

Самое частое применение множества в одну строку.

main.pyуникальные теги
tags = ["python", "api", "python", "sql"]

print(sorted(set(tags)))
Выводтри уникальных значения
['api', 'python', 'sql']

Способ подходит, когда порядок не важен: обратное преобразование list(set(tags)) исходный порядок не восстанавливает.

§14Уникальность с порядком

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

main.pyключи словаря как упорядоченное множество
tags = ["python", "api", "python", "sql"]

print(list(dict.fromkeys(tags)))
Выводпорядок первого появления сохранён
['python', 'api', 'sql']

dict.fromkeys() из главы 4.3 создаёт словарь, где ключи — элементы списка, а значения не нужны. Приём стоит запомнить: он решает задачу, которую множество решить не может.

§15Практика: уже обработанные

Второе классическое применение — запоминать, что уже встречалось.

main.pyкаждый идентификатор обрабатывается один раз
task_ids = [101, 103, 101, 108, 103]
processed = set()

for task_id in task_ids:
    if task_id in processed:
        continue
    print("обработка", task_id)
    processed.add(task_id)
Выводповторы пропущены
обработка 101
обработка 103
обработка 108

Множество здесь делает сразу два дела: хранит уникальные идентификаторы и мгновенно отвечает, встречался ли каждый следующий. Со списком проверка in замедлялась бы по мере роста данных.

§16Когда set, когда list

НужноТип
порядок элементовlist
повторы имеют смыслlist
доступ по индексуlist
убрать повторыset
частые проверки принадлежностиset
сравнить два набора между собойset
уникальность и порядок сразуdict.fromkeys()

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

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

Что написаноЧто произойдётКак правильно
empty = {}создастся словарь, а не множествоempty = set()
values[0]TypeError: индексов нетперебрать или взять список
values.add([1, 2])TypeError: unhashable type: 'list'добавить кортеж (1, 2)
полагаться на порядок выводаон может измениться при других данныхsorted(values)
list(set(items)) ради уникальности и порядкаисходный порядок потеряетсяlist(dict.fromkeys(items))
ждать, что {1, True} даст два элементаостанется один: значения равныне смешивать bool и числа
values.add(x) для существующего элементаничего не произойдёт, но и ошибки не будетэто нормально: повторов в множестве нет
set внутри setTypeError: множество изменяемоfrozenset из главы 4.9

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

Чем пустое множество отличается по записи от пустого словаря?

Множество создают только как set(): литерал {} всегда даёт словарь.

Почему у множества нет индексов?

Элементы лежат в хеш-таблице по своим хешам, а не по позициям, поэтому понятий «первый» и «второй» не существует.

Почему проверка in в множестве быстрее, чем в списке?

В списке элементы сравниваются по очереди, а в множестве Python по хешу сразу вычисляет, где значение должно лежать.

Сколько элементов будет в {1, 1.0, True}?

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

Как убрать повторы, сохранив порядок?

list(dict.fromkeys(items)): ключи словаря уникальны и сохраняют порядок добавления.

Когда список подходит лучше множества?

Когда важен порядок, нужен доступ по индексу или повторы несут смысл.

§19Шпаргалка

Создатьvalues = {1, 2, 3}
Пустоеempty = set()
Убрать повторыunique = set(items)
С порядкомlist(dict.fromkeys(items))
Проверитьvalue in values
Добавитьvalues.add(value)
Предсказуемый выводfor v in sorted(values):
Быстрый поискknown = set(big_list)

Итог главы

Множество — это словарь без значений: та же хеш-таблица, поэтому элементы уникальны, а проверка принадлежности не зависит от размера коллекции. У множества нет индексов и нет гарантированного порядка, и для воспроизводимого вывода нужен sorted(). Элементы должны быть хешируемыми, а равные значения разных типов считаются одним элементом. Если вместе с уникальностью нужен порядок первого появления, множество не подходит — подходит dict.fromkeys().

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