Множества (set) в Python

Что в них хранится и как этим пользоваться

Множества (set) в Python

Python до сих пор остаётся самым популярным языком программирования в мире по многим причинам: он удобный, простой для изучения, многофункциональный. 

Ещё Python очень хорошо реализован, и это внутреннее устройство особенно проявляется на его коллекциях. Это наборы разных элементов, структурированных определённым образом. В одной коллекции можно хранить что угодно, в другой элементы обязательно должны лежать по парам, какие-то нельзя изменять или хранить внутри них изменяемые элементы.

Сегодня рассказываем про множества, которые в материалах по Python часто называют ключевым словом set. Расскажем, как их создавать и работать с ними, и немного разберёмся, как они реализованы на уровне кода.

Особенности множеств в Python

Множества в Python обозначаются фигурными скобками. Такое же обозначение использует другая коллекция — словарь. Различить их просто, потому что визуально они сильно отличаются друг от друга.

Чтобы научиться узнавать множества и словари на практике, создадим два небольших примера. Сначала объявим множество, в котором будут название фильма, дата выхода и имена трёх актёров. В типах данных это строка, целое число и кортеж из трёх элементов:

my_set = {‘Alien’, 1979, (‘Sigourney Weaver’, ‘Tom Skerritt’, ‘Veronica Cartwright’)}

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

my_dict = {‘Ridley Scott’ : 1937,
‘James Cameron’ : 1954,
‘David Fincher’ : 1962}

Для эффективной работы с множествами полезно помнить ещё несколько вещей.

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

Множества (set) в Python

Элементы множества должны быть неизменяемыми, но сами множества менять можно — например, добавить или удалить элемент. Поэтому нельзя записать во множество список, словарь или другое множество.

Ещё элементы множества всегда уникальные. Если попробовать сохранить несколько одинаковых значений, то при выводе будет видно, что сохранилось только одно:

# создаём множество одинаковых элементов
my_set = {elements, data, operations, data, elements}
# выводим его на экран
print(my_set)

Результатом будут элементы без повторов:

{elements, data, operations}

Теперь представьте, что у вас есть база данных электронных почт зарегистрированных клиентов, в которой нужно найти только уникальные значения. Достаточно просто записать все адреса во множество, и Python сам удалит дубликаты.

Основные принципы устройства множеств

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

Магические методы

Python — это объектно-ориентированный язык программирования. Все элементы внутри кода — это объекты. 

На уровне основного устройства в Питоне определены правила и свойства одного, самого начального объекта. Эти свойства программа умеет встраивать во все остальные объекты — говорят, что они наследуются от основного объекта. Но встраиваются они неявно: мы в программе это не видим, а просто используем. Некоторые свойства будут отключены, это зависит от того, что именно мы создаём.

Невидимые свойства и возможности основного объекта называются магическими методами. Если программист всё-таки решил поработать с магическим методом так, чтобы это было явно видно в программе, то узнать их в коде можно по двойным подчёркиваниям с двух сторон от названия.

Чаще всего мы видим магические методы, когда работает с классами:

# объявляем класс для примера
class Example:
   # двойное подчёркивание: магический метод
   def __init__(self):
       pass
   # без подчёркивания: обычный метод класса
   def square(self):
       pass

Хеши и хеш-таблицы

Хеши — это как отпечатки пальцев для любого файла. Специальная хеш-функция шифрует данные так, что в конце получается строка из символов, которая почти ничего не весит. По этой строке нельзя восстановить исходный файл, и при использовании хорошей функции все хеши будут уникальными.

Любое изменение в файле отразится на хеше: даже если в романе на 400 страниц заменить одну букву или знак препинания, весь хеш изменится.

В Python есть встроенный магический метод __hash__ для хеширования, который иногда создаёт одинаковые хеши, зато работает очень быстро. Этот метод часто используется в коллекциях, чтобы определить место хранения элементов в оперативной памяти. 

То есть получается так: 

  • программа вычисляет хеш объекта;
  • по этому хешу вычисляет номер ячейки памяти;
  • отправляет объект в ячейку.

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

Иногда встроенная хеш-функция может выдать у разных данных одинаковый хеш. Если такое происходит, множество сравнивает элементы напрямую, чтобы убедиться, что они на самом деле одинаковые. Если одинаковые — программа удаляет дублирующий элемент. Если разные, то просто перебирает следующие ячейки памяти, пока не находит свободную.

Такое устройство называется хеш-таблицей: это одномерная таблица, в которой каждой ячейке соответствует определённый элемент, и они не обязательно идут подряд. Хеши внутри таблицы не хранятся, но они нужны для вычисления номеров ячеек. Это можно изобразить примерно так:

Ячейка 0 | [Элемент 1]
Ячейка 1 | []
Ячейка 2 | [Элемент 2]
Ячейка 3 | []
Ячейка 4 | [Элемент 3]

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

Другое следствие такого устройства работы множества — внутри не может быть два одинаковых элемента, потому что их хеши тоже будут одинаковыми. А одинаковые хеши ведут к одной ячейке памяти.

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

Для создания множеств есть несколько вариантов. Мы разберём каждый с примерами, чтобы было видно, как это работает.

Способы создания множеств

Самый простой способ — сразу записать значения, которые нам нужны. Для этого нужно объявить переменную и перечислить внутри фигурных скобок элементы через запятую:

# создаём множество из строк
androids = {‘Ash’, ‘Bishop’, ‘Call’, ‘David’}

Использование функции set()

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

Как это выглядит:

# создаём пустую коллекцию элементов
empty_collection = {}
# проверяем её тип
print(type(empty_collection))

Теперь при запуске кода можно посмотреть, что у нас получился словарь:

Но если использовать круглые скобки и поставить перед ними ключевое слово set, Python создаст множество:

# создаём пустое множество
empty_collection = set()
# проверяем тип созданного объекта
print(type(empty_collection))

В консоли терминала получаем:

Обратите внимание, что скобки используются именно круглые, потому что set в Python — функция, которая принимает в скобках аргументы. 

Мы можем передать скобки внутри функции обычный список, а set() трансформирует этот список во множество. Но для этого в списке не должно быть изменяемых элементов.

Поэтому можно сделать так:

# создаём список
list_for_set = [1, 2, 3, 4, 5, 4, 3, 2, 1]
# передаём список в функцию
new_set = set(list_for_set)
# выводим функцию на экран
print(new_set)

И получить множество из уникальных элементов списка:

{1, 2, 3, 4, 5}

Если попробовать передать список, внутри которого будут другие списки, ничего не получится, потому что это изменяемый тип данных:

# создаём список из других списков
list_for_set = [[1, 2], [3, 4], [5, 6]]
# передаём список в функцию
new_set = set(list_for_set)
# выводим функцию на экран
print(new_set)

При запуске получаем ошибку такого вида:

TypeError: unhashable type: ‘list’

Генераторы множеств

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

Пример — идём по списку целых чисел от 0 до 9 и, если число чётное, возводим его в квадрат и добавляем во множество:

# создаём генератор, который считает
# квадраты чисел и добавляет их во множество
squares = {x**2 for x in range(10) if x % 2 == 0}
# выводим результат на экран
print(squares)

Получаем набор возведённых в квадрат чисел. Обратите внимание, что при выводе они чаще всего будут идти беспорядочно, а не по возрастанию:

{0, 64, 4, 36, 16}

Добавление элементов

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

Для добавления нового элемента у множества нужно вызвать метод .add. Чтобы посмотреть, как это работает, создадим пустое множество и добавим в него два элемента, причём один и тот же элемент попробуем вставить дважды.

# создаём пустое множество
ships = set()
# добавляем элемент
ships.add("Nostromo")
# пробуем добавить дубликат
ships.add("Nostromo")
# добавляем другой элемент
ships.add("Enterprise")
# выводим результат на экран
print(ships)

Проверяем, что во множестве сохранились только уникальные элементы:

{Nostromo, Enterprise}

Удаление элементов

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

  • .remove вызовет ошибку, если попытаться удалить несуществующий элемент;
  • .discard не вызовет ошибку, даже если указанного элемента во множестве нет.

Смотрим на примерах. Попробуем запустить такую программу:

# создаём множество
ships = {"Nostromo", "Enterprise"}
# вызовет ошибку, если элемента нет
ships.remove("Millennium Falcon")

Получаем:

KeyError: Millennium Falcon

Теперь попробуем удалить этот же элемент, но через .discard:

# создаём множество
ships = {"Nostromo", "Enterprise"}
# безопасное удаление без ошибки
# не вызовет ошибку, даже если элемента нет
ships.discard("Millennium Falcon")
# проверяем результат
print(ships)

При запуске видно, что программа выполнила строку с удалением несуществующего элемента “Millennium Falcon” без ошибок:

{Nostromo, Enterprise}

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

Для перебора можно использовать цикл for. Он пройдёт по элементам хеш-таблицы в том порядке, каком они записаны.

Например, мы можем вывести все элементы на экран:

# создаём множество
ships = {"Nostromo", "Enterprise", "Millennium Falcon"}
# перебираем все элементы и выводим их на экран
for ship in ships:
  print(ship)

Результат в консоли:

Nostromo
Enterprise
Millennium Falcon

Проверка наличия элемента

Чтобы узнать, есть ли элемент во множестве или нет, можно воспользоваться оператором in и посмотреть на результат выражения вида «Элемент 1 находится во Множестве А». Если он действительно находится во множестве, выражение вернёт значение True. Если элемента нет, вернётся False.

В коде будет выглядеть так:

# создаём множество
ships = {"Nostromo", "Enterprise", "Millennium Falcon"}
# получаем булево значение для вхождения разных элементов во множество
print("Nostromo" in ships)
print("Normandy SR-2" in ships)

А при запуске — так:

True
False

Математические операции с множествами в Python

Это операции, которые применяются не только в программировании, но и в математике. В математике они обозначаются своими нотациями (то есть специальными символами), а в программировании — своими.

Вот основные.

Объединение множеств

Если у нас есть два множества, объединить их элементы можно через оператор | или метод .union(). При этом одинаковые данные удалятся.

Смотрим пример, где в двух множествах есть одинаковый элемент — 3.

# создаём два множества
a = {1, 2, 3}
b = {3, 4, 5}
# объединяем оба множества, вариант 1
union1 = a | b
# объединяем оба множества, вариант 2
union2 = a.union(b)
# выводим результат на экран
print(union1)
print(union2)

Проверяем, что получилось в обоих случаях:

{1, 2, 3, 4, 5}

Видно, что из двух одинаковых элементов остался только один.

Пересечение множеств

Находит одинаковый элемент в обоих множествах. Работает через оператор &.

Пример:

# создаём два множества
a = {1, 2, 3}
b = {3, 4, 5}
# объединяем оба множества
intersection = a & b
# выводим результат на экран
print(intersection)

В консоли получаем два одинаковых вывода:

{3}

Разность множеств

Разность показывает, какие элементы есть только в одном из множеств. Для этого применяется знак вычитания :

# создаём два множества
a = {1, 2, 3}
b = {3, 4, 5}
# получаем разницу множеств
difference = a - b
print(difference)

Проверяем вывод:

{1, 2}

Симметрическая разность множеств

Находит элементы, уникальные для каждого множества. Это значит, что элемент есть либо в одном множестве, либо в другом, но не обоих сразу. Для использования нужен оператор ^ или метод .symmetric_difference():

# создаём два множества
a = {1, 2, 3}
b = {3, 4, 5}
# симметричная разница, вариант 1
symmetric_diff1 = a ^ b
# симметричная разница, вариант 2
symmetric_diff2 = a.symmetric_difference(b)
# выводим результат на экран
print(symmetric_diff1)
print(symmetric_diff2)

При запуске видно, что метод исключил 3 — общий элемент:

{1, 2, 4, 5}

Методы работы с множествами в Python

Методы — встроенные возможности коллекции, которые вызываются через точку после названия переменной. После названия метода ставятся скобки, если мы хотим, чтобы он сработал. Иногда метод нужно передать или вызвать как объект, тогда скобки не нужны.

Метод copy()

Создаёт точную копию множества с такими же элементами. Если эту копию потом как-то изменить, то на оригинале это не отразится.

Команда для вызова — .copy().

Для примера скопируем множество и добавим новый элемент только в копию:

# создаём множество
terran_units = {"Marine", "Tank", "Medivac"}

# создаём копию множества
backup = terran_units.copy()

# добавляем новый элемент только в копию
backup.add("Viking")

# выводим оригинал и копию
print("Оригинал:", terran_units)
print("Копия:", backup)

После запуска программы видно, что оригинал не изменился:

Оригинал: {‘Medivac’, ‘Tank’, ‘Marine’}
Копия: {‘Medivac’, ‘Viking’, ‘Tank’, ‘Marine’}

Метод isdisjoint()

Делает то же самое, что пересечение множеств: возвращает значение True, если множества не пересекаются и не имеют одинаковых элементов. Если одинаковые элементы есть, возвращает False.

Для вызова нужно добавить команду .intersection().

Попробуем на примере:

# создаём множество 1
protoss_units = {"Zealot", "Dragoon", "High Templar"}

# создаём множество 2
zerg_units = {"Zergling", "Hydralisk", "Mutalisk"}

# проверяем, что общих элементов нет
no_common = protoss_units.isdisjoint(zerg_units)

# выводим результат
print(no_common)

Одинаковых элементов нет, поэтому запущенная программа возвращает:

True

Метод len()

Эту функцию часто применяют отдельно, и она работает по-разному с разными объектами в Python. Для множества возвращает количество уникальных элементов.

Работает через команду .len():

# создаём множество
terran_units = {"Marine", "Tank", "Medivac"}

# считаем количество элементов
count = len(terran_units)

# выводим результат
print(count)

Проверяем, что выводится в консоли:

3

Иммутабельные множества (frozenset)

Обычное множество в Python можно изменять, поэтому его нельзя использовать как элементы другого множества или ключи для словарей. Но из обычного можно получить иммутабельное, или замороженное, которое менять нельзя и можно использовать как любой другой неизменяемый объект.

Для практики попробуем создать такое множество и добавить новый элемент:

# создаём обычное множество
zerg_units = {"Zergling", "Hydralisk", "Mutalisk"}

# создаём замороженное множество из обычного
frozen_zerg = frozenset(zerg_units)

# пытаемся добавить элемент
frozen_zerg.add("Ultralisk")

Получаем ошибку, потому что замороженное множество нельзя изменять:

AttributeError: ‘frozenset’ object has no attribute ‘add’

Бонус для читателей

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

Вам слово

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

Обложка:

Алексей Сухов

Корректор:

Александр Зубов

Вёрстка:

Мария Климентьева

Соцсети:

Юлия Зубарева

Вам может быть интересно
easy