Множества set в Python: что это и как с ними работать

Множества set в Python: что это и как с ними работать


Коротко. Множество (set) — это изменяемая коллекция уникальных неупорядоченных элементов. Дубликаты в нее попасть не могут, обращения по индексу нет, зато проверка наличия элемента работает почти мгновенно независимо от размера коллекции. Создают множество фигурными скобками {1, 2, 3} или функцией set(), а хранить внутри можно только неизменяемые объекты — числа, строки, кортежи.


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


У новичка обычно есть списки, и кажется, что их хватает на все. Но три задачи списки решают плохо, а множества — отлично.


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


Быстро проверить наличие. Есть база из миллиона email, и нужно понять, встречается ли в ней конкретный адрес. В списке Python будет перебирать элементы по одному, во множестве — найдет ответ сразу.


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


emails = ["a@mail.uz", "b@mail.uz", "a@mail.uz"]


unique = set(emails)


print(unique)


# {'a@mail.uz', 'b@mail.uz'}


Три свойства множества


Уникальность. Каждый элемент встречается ровно один раз. Попытка добавить дубликат ничего не изменит и ошибки не вызовет.


Неупорядоченность. У элементов нет позиций и порядкового номера. Обратиться к my_set[0] нельзя, срезы тоже не работают.


Изменяемость. Элементы можно добавлять и удалять — в отличие от кортежа.


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


Как создать множество


Фигурными скобками — самый короткий способ:


courses = {"Python", "QA", "Frontend"}


Функцией set() из любого итерируемого объекта:


print(set([1, 2, 2, 3])) # {1, 2, 3}


print(set((1, 2, 3))) # {1, 2, 3}


print(set("привет")) # {'п', 'р', 'и', 'в', 'е', 'т'}


print(set(range(1, 4))) # {1, 2, 3}


print(set({"a": 1, "b": 2})) # {'a', 'b'} — только ключи


Генератором множества — по аналогии с генератором списка, но в фигурных скобках:


squares = {x ** 2 for x in range(10) if x % 2 == 0}


print(squares) # {0, 4, 16, 36, 64}


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


empty = {}


print(type(empty)) # <class 'dict'>


empty = set()


print(type(empty)) # <class 'set'>


Пустое множество создается только через set(). Это одна из самых частых ошибок новичков.


Что можно положить во множество


Только неизменяемые объекты: числа, строки, кортежи, frozenset, None, логические значения.


valid = {1, 3.14, "текст", (1, 2), None, True}


Списки, словари и обычные множества внутрь не поместить:


{1, [2, 3]}


# TypeError: unhashable type: 'list'


Ошибка unhashable type означает ровно это: объект изменяемый, посчитать для него постоянный хеш нельзя.


Если вложенная коллекция все же нужна, преобразуйте ее в кортеж или frozenset:


pairs = {(1, 2), (3, 4)}


groups = {frozenset([1, 2]), frozenset([3, 4])}


Неочевидная деталь. Python считает равными 1, 1.0 и True — у них одинаковый хеш. Поэтому во множестве останется только первое добавленное:


print({1, True, 1.0}) # {1}


Это редко мешает, но иногда объясняет, почему из набора «пропал» элемент.


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


add() добавляет один элемент:


courses = {"Python", "QA"}


courses.add("Frontend")


courses.add("Python") # уже есть, ничего не изменится


print(len(courses)) # 3


update() добавляет сразу несколько из любого итерируемого объекта:


courses = {"Python"}


courses.update(["QA", "Frontend"])


courses.update({"Blender"})


print(courses)


# {'Python', 'QA', 'Frontend', 'Blender'}


Осторожно со строкой: как и extend у списков, update разберет ее на символы.


tags = set()


tags.update("smm")


print(tags) # {'s', 'm'} — не то, что ожидалось


Для добавления строки целиком нужен add().


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


Способов четыре, и они различаются поведением.


remove() удаляет элемент и падает с ошибкой KeyError, если его нет:


courses = {"Python", "QA"}


courses.remove("QA")


courses.remove("Blender") # KeyError: 'Blender'


discard() делает то же самое, но молчит, если элемента не было. Для данных из внешних источников он безопаснее.


pop() удаляет произвольный элемент и возвращает его. Какой именно — предсказать нельзя, потому что порядка у множества нет. На пустом множестве вызывает KeyError.


clear() полностью очищает множество.


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


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


Объединение — все элементы обоих множеств:


group_a = {"Азиз", "Дилноза", "Тимур"}


group_b = {"Тимур", "Малика"}


print(group_a | group_b)


print(group_a.union(group_b))


# {'Азиз', 'Дилноза', 'Тимур', 'Малика'}


Пересечение — только общие элементы:


print(group_a & group_b)


print(group_a.intersection(group_b))


# {'Тимур'}


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


print(group_a - group_b)


print(group_a.difference(group_b))


# {'Азиз', 'Дилноза'}


Порядок здесь важен: group_b - group_a даст уже {'Малика'}.


Симметрическая разность — элементы, которые есть только в одном из множеств:


print(group_a ^ group_b)


print(group_a.symmetric_difference(group_b))


# {'Азиз', 'Дилноза', 'Малика'}


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


group_a.union(["Малика", "Санжар"]) # работает


group_a | ["Малика"] # TypeError


Так что при работе со списками методы удобнее — не нужно предварительно все преобразовывать.


У каждой операции есть версия, которая меняет множество на месте: update, intersection_update, difference_update, symmetric_difference_update, а также операторы |=, &=, -=, ^=.


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


Наличие элемента проверяется оператором in:


courses = {"Python", "QA"}


print("Python" in courses) # True


print("Blender" not in courses) # True



Множества можно сравнивать между собой:


  • == — множества полностью совпадают по составу, порядок значения не имеет
  • <= и issubset() — все элементы первого есть во втором
  • < — то же самое, но множества не равны
  • >= и issuperset() — первое содержит второе целиком
  • isdisjoint() — у множеств нет ни одного общего элемента


juniors = {"Python", "SQL"}


required = {"Python", "SQL", "Git"}


print(juniors <= required) # True — подмножество


print(required.issuperset(juniors)) # True


print(juniors.isdisjoint({"Java"})) # True — пересечений нет


Обратите внимание: == проверяет полное равенство, а не наличие общих элементов. Чтобы узнать, пересекаются ли множества, используйте & или isdisjoint().


Насколько множества быстрее списков


Это главный практический аргумент в пользу set.


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


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


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


# медленно


blacklist = load_blacklist() # список из 100 000 адресов


for email in emails:


if email in blacklist:


...


# быстро


blacklist = set(load_blacklist())


for email in emails:


if email in blacklist:


...


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


Удаление дубликатов и потеря порядка


Самый популярный способ применения множеств выглядит так:


names = ["Азиз", "Дилноза", "Азиз", "Тимур"]


unique = list(set(names))


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


Если порядок важен, используйте словарь — с версии Python 3.7 он гарантированно запоминает порядок добавления ключей:


names = ["Азиз", "Дилноза", "Азиз", "Тимур"]


unique = list(dict.fromkeys(names))


print(unique)


# ['Азиз', 'Дилноза', 'Тимур']


Этот прием стоит запомнить: он решает ту же задачу, но без потери последовательности.


frozenset: неизменяемое множество


frozenset — версия множества, которую нельзя менять после создания. Методов add, remove и update у него нет, зато доступны все операции сравнения и объединения.


tags = frozenset(["python", "backend"])


tags.add("sql") # AttributeError


print(tags | {"sql"}) # frozenset({'python', 'backend', 'sql'})


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


permissions = {


frozenset(["read"]): "Читатель",


frozenset(["read", "write"]): "Редактор",


}


Второе применение — защита данных. Если набор не должен меняться (роли, допустимые значения, конфигурация), frozenset гарантирует, что никакая функция его случайно не испортит.


Практические примеры


Что изменилось между двумя выгрузками. Классическая задача аналитика: сравнить вчерашний и сегодняшний список клиентов.


yesterday = {"A101", "A102", "A103"}


today = {"A102", "A103", "A104"}


print(today - yesterday) # {'A104'} — новые


print(yesterday - today) # {'A101'} — ушедшие


print(today & yesterday) # общие


print(today ^ yesterday) # все изменения разом


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


required = {"Python", "SQL", "Git"}


candidate = {"Python", "SQL", "Excel"}


missing = required - candidate


if missing:


print("Не хватает навыков:", ", ".join(missing))


Фильтрация стоп-слов в тексте.


stop_words = {"и", "в", "на", "с", "по"}


text = "курсы по python в ташкенте"


words = [w for w in text.split() if w not in stop_words]


print(words) # ['курсы', 'python', 'ташкенте']


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


Поиск дубликатов, а не их удаление.


values = [1, 2, 3, 2, 4, 1]


seen = set()


duplicates = set()


for value in values:


if value in seen:


duplicates.add(value)


seen.add(value)


print(duplicates) # {1, 2}


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


Пустые фигурные скобки. {} создает словарь, а не множество. Нужен set().


Попытка обратиться по индексу. my_set[0] вызовет TypeError — у множества нет позиций. Чтобы перебрать элементы, используйте цикл for, а чтобы получить конкретный — преобразуйте в список.


Ожидание сохраненного порядка. Порядок вывода множества не определен и может меняться между запусками. Для упорядоченного результата используйте sorted().


Список внутри множества. Вызывает TypeError: unhashable type. Преобразуйте в кортеж.


update со строкой. Разбирает строку на символы. Для добавления строки целиком нужен add().


Оператор вместо метода со списком. my_set | [1, 2] не сработает, а my_set.union([1, 2]) — да.


remove вместо discard. Если элемента может не быть, remove уронит программу с KeyError.


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


Когда брать множество, а когда список


  • Важен порядок элементов или нужны индексы — список
  • Могут быть повторы и они значимы — список
  • Нужна уникальность — множество
  • Часто проверяется наличие элемента в большой коллекции — множество
  • Нужно сравнить две коллекции, найти общее или различия — множество
  • Данные не должны меняться и должны быть ключом словаря — frozenset
  • Нужны пары «ключ — значение» — словарь



Коротко о главном


Множество в Python хранит уникальные элементы, не помнит их порядка и не дает обращаться по индексу. Взамен оно почти мгновенно отвечает на вопрос «есть ли здесь такой элемент» и умеет за один оператор находить объединение, пересечение и разность двух коллекций.


Три вещи, которые стоит запомнить сразу: пустое множество создается только через set(), внутрь можно класть лишь неизменяемые объекты, а при удалении дубликатов через set теряется исходный порядок — если он нужен, берите dict.fromkeys().


Понимание структур данных и того, какая из них подходит под задачу, отличает работающий код от быстрого. Освоить Python с нуля до уровня реальных проектов можно на курсе Python в PROWEB — 8 месяцев, с 14 лет, с практикой и поддержкой в трудоустройстве. Если вы еще выбираете направление в IT, пройдите бесплатный IT-тест — он подскажет, что вам ближе.


Часто задаваемые вопросы


Что такое множество в Python?


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


Чем множество отличается от списка?


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


Как создать пустое множество?


Только функцией set(). Пустые фигурные скобки {} создают словарь — это одна из самых распространенных ошибок новичков.


Почему нельзя добавить список во множество?


Потому что элементы хранятся по хешу, а у изменяемого объекта хеш непостоянен. Python выдаст ошибку TypeError: unhashable type: 'list'. Если нужна вложенная коллекция, преобразуйте список в кортеж или frozenset.


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


Через list(dict.fromkeys(items)). Обычное преобразование в множество дубликаты уберет, но исходный порядок при этом потеряется.


Чем отличаются remove и discard?


Оба удаляют элемент, но remove вызывает ошибку KeyError, если элемента нет, а discard в этой ситуации ничего не делает. Для данных из внешних источников безопаснее discard.


Чем frozenset отличается от set?


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


Как найти общие элементы двух списков?


Преобразовать оба в множества и взять пересечение: set(a) & set(b). Тот же результат даст метод intersection, который к тому же принимает список напрямую, без преобразования.


Сохраняется ли порядок элементов во множестве?


Нет. Порядок вывода не определен и может отличаться между запусками программы. Если нужен предсказуемый порядок, применяйте sorted() при выводе.


Насколько множества быстрее списков при поиске?


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



Другие статьи