$ sudo teach IT
Модуль 5 · Коллекции · Урок 5.7

Множества

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

Теория~30 минутНовичокset()frozenset()УникальностьОбъединение и пересечениеРазностьПодмножества

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

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

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

У множества три особенности, которые стоит запомнить сразу:

1. Уникальность — если попытаться положить в множество то, что там уже есть, ничего не изменится: дубликата не появится

2. Неупорядоченность — у элементов множества нет номера места, обратиться «к первому элементу» нельзя

3. Изменяемость — элементы можно добавлять и убирать уже после создания множества

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

Пустые фигурные скобки — это не множество

Здесь чаще всего ошибаются даже те, кто уже неплохо пишет на Python. Если раньше вы разбирали словари, то знаете, что фигурные скобки {} создают словарь. С множествами это создаёт путаницу: хочется по аналогии написать {} и получить пустое множество, но так не получится.

a = {}
print(type(a))

b = set()
print(type(b))

Разбираем построчно:

  • a = {} — пустые фигурные скобки без содержимого. Python читает это как пустой словарь, а не множество: так исторически устроен язык.
  • print(type(a)) — выводит тип переменной a. На экране будет <class 'dict'>, то есть словарь.
  • b = set() — вызов функции set() без аргументов. Вот это и есть правильный способ создать пустое множество.
  • print(type(b)) — выведет <class 'set'>: теперь перед нами действительно множество.
Запомните раз и навсегда: {} без содержимого — всегда пустой словарь. Пустое множество создаётся только вызовом set().

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

s = {1, 2, 3}
d = {"a": 1, "b": 2}
  • s = {1, 2, 3} — в фигурных скобках просто перечислены значения через запятую, без двоеточий. Это множество из трёх чисел.
  • d = {"a": 1, "b": 2} — здесь у каждого элемента есть пара «ключ: значение». Это словарь.

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

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

fruits = {"яблоко", "банан", "яблоко", "вишня", "банан"}
print(fruits)

numbers = set([1, 2, 2, 3, 3, 3, 4])
print(numbers)

letters = set("привет")
print(letters)
  • fruits = {"яблоко", "банан", "яблоко", "вишня", "банан"} — перечислили пять слов через запятую в фигурных скобках, но два слова повторяются.
  • print(fruits) — выведет {'яблоко', 'банан', 'вишня'}: три уникальных значения, повторы исчезли сами. Обратите внимание и на то, что порядок вывода может не совпадать с порядком записи.
  • numbers = set([1, 2, 2, 3, 3, 3, 4]) — функция set() принимает список и строит из его элементов множество. Это самый частый способ превратить готовый список в множество.
  • print(numbers) — выведет {1, 2, 3, 4}: числа без повторов.
  • letters = set("привет") — set() умеет принимать и строку. Строка — это последовательность символов, и каждый символ становится отдельным элементом множества.
  • print(letters) — выведет множество из уникальных букв слова «привет», например {'п', 'р', 'и', 'в', 'е', 'т'} (все буквы здесь разные, поэтому ни одна не потерялась).
Практический приём — убрать дубликаты из списка: list(set(мой_список)) одной строкой превращает список в множество и обратно в список, по пути избавившись от повторов. Единственная плата за это — порядок элементов может перемешаться.

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

У списка есть пронумерованные места: lst[0] — первый элемент, lst[1] — второй. У множества такой нумерации нет в принципе, ведь порядок элементов не гарантирован и может отличаться от того, в котором вы их перечисляли.

s = {5, 2, 8, 1}
print(s)

for item in s:
    print(item, end=" ")
  • s = {5, 2, 8, 1} — создали множество из четырёх чисел именно в этом порядке записи.
  • print(s) — выведет, например, {1, 2, 5, 8}: порядок необязательно совпадёт с тем, в каком числа перечислялись при создании.
  • for item in s: — перебор множества циклом for работает, как и для любой другой коллекции: каждая буква строки, элемент списка или, как здесь, элемент множества по очереди попадает в item.
  • print(item, end=" ") — печатает каждый элемент через пробел вместо перевода строки, но в каком порядке они появятся — заранее не известно.

Обращение по номеру, как у списка (s[0]), для множества вызовет ошибку TypeError: 'set' object is not subscriptable — «объект нельзя обращаться по индексу».

Если важен порядок — используйте список. Множество нужно тогда, когда важна именно принадлежность («есть элемент или нет»), а не его место.

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

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

Официально это методы множества: add() добавляет один объект, update() добавляет все элементы переданного итерируемого объекта (списка, строки, другого множества).

tags = {"python", "code"}

tags.add("beginner")
print(tags)

tags.add("python")
print(tags)

tags.update(["web", "backend"])
print(tags)
  • tags = {"python", "code"} — множество из двух строк.
  • tags.add("beginner") — добавляет одну новую строку в множество. У множества, в отличие от списка, нет метода append — для множеств он называется add.
  • print(tags) — теперь в множестве три элемента: {'python', 'code', 'beginner'} (порядок вывода может быть любым).
  • tags.add("python") — пытаемся добавить то, что уже есть в множестве.
  • print(tags) — множество не изменилось: три элемента, как и было. Никакой ошибки при этом не возникает — повтор просто не попадает внутрь.
  • tags.update(["web", "backend"]) — передали список из двух строк, и обе сразу добавились в множество.
  • print(tags) — теперь пять элементов: {'python', 'code', 'beginner', 'web', 'backend'}.

Удаление элементов: remove, discard, pop

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

colors = {"red", "green", "blue"}

colors.remove("green")
print(colors)

colors.discard("pink")
print(colors)

item = colors.pop()
print(item)
  • colors.remove("green") — убирает элемент "green" из множества. Если бы такого элемента не было, метод бы бросил ошибку KeyError.
  • print(colors) — останутся два элемента: {'red', 'blue'}.
  • colors.discard("pink") — пытаемся убрать элемент, которого в множестве нет. В отличие от remove, метод discard в такой ситуации просто ничего не делает, без ошибки.
  • print(colors) — множество не изменилось: {'red', 'blue'}.
  • item = colors.pop() — удаляет из множества один элемент и одновременно возвращает его. Так как порядок не гарантирован, заранее не известно, какой именно элемент достанется — "red" или "blue".
  • print(item) — выведет тот элемент, который pop() вернул и одновременно убрал из множества.

Когда что использовать:

remove() — когда уверены, что элемент есть, и хотите узнать об ошибке, если это не так.

discard() — когда не уверены, есть ли элемент: безопаснее, программа не упадёт.

pop() — когда нужен просто какой-нибудь элемент множества, неважно какой именно.

Операции с множествами: объединение, пересечение, разность

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

alice = {"Матрица", "Дюна", "Начало"}
bob = {"Дюна", "Начало", "Аватар"}

Две переменные-множества: alice хранит три названия фильмов, bob — тоже три, причём «Дюна» и «Начало» встречаются у обоих.

Объединение | — «всё, что смотрел хотя бы один из двух»

all_films = alice | bob
print(all_films)

Символ | между двумя множествами — «объединение». Он собирает все элементы обоих множеств в одно, без повторов. Результат: {'Матрица', 'Дюна', 'Начало', 'Аватар'}.

Пересечение & — «только то, что смотрели оба»

common = alice & bob
print(common)

Символ & оставляет только те элементы, которые есть в обоих множествах сразу. Результат: {'Дюна', 'Начало'}.

Разность - — «у Алисы есть, у Боба нет»

only_alice = alice - bob
print(only_alice)

alice - bob оставляет только те элементы alice, которых нет в bob. Результат: {'Матрица'}. Разность несимметрична: bob - alice дало бы другой ответ — {'Аватар'}.

Симметричная разность ^ — «есть у одного, но не у обоих сразу»

unique_to_each = alice ^ bob
print(unique_to_each)

^ оставляет всё, что встречается ровно в одном из двух множеств, и убирает то, что общее. Результат: {'Матрица', 'Аватар'} — это объединение alice | bob, из которого убрали общую часть alice & bob.

Подмножества: issubset и in

Простыми словами: иногда нужно узнать не «что общего», а «полностью ли одно множество умещается внутри другого». Например, все ли обязательные темы курса уже пройдены студентом.

За это отвечает метод issubset() — «является подмножеством». Есть и обратный метод, issuperset() — «является надмножеством», то есть содержит ли множество все элементы другого.

beginners = {"python", "variables", "loops"}
full_course = {"python", "variables", "loops", "sets", "files"}

print(beginners.issubset(full_course))
print(full_course.issubset(beginners))
print("python" in full_course)
  • beginners.issubset(full_course) — спрашиваем: все ли элементы beginners есть внутри full_course? Да, все три темы там есть, поэтому будет True.
  • full_course.issubset(beginners) — а теперь наоборот: весь ли full_course умещается в beginners? Нет, в full_course есть темы, которых в beginners нет, поэтому False.
  • "python" in full_course — проверка принадлежности через in работает у множества так же, как у строки или списка: True, если элемент есть, иначе False. Здесь будет True.
Почему множество, а не список. Проверка x in множество выполняется практически мгновенно, независимо от того, сколько в нём элементов. У списка та же проверка требует по очереди просмотреть все элементы. Если элементов много и принадлежность нужно проверять часто — множество быстрее.

frozenset: множество, которое нельзя изменить

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

Для этого есть frozenset — «замороженное множество». Оно ведёт себя как обычное множество, но после создания его нельзя изменить: ни добавить элемент, ни убрать.

fs = frozenset(["read", "write"])
print(fs)

fs2 = frozenset(["write", "delete"])
print(fs & fs2)
  • fs = frozenset(["read", "write"]) — функция frozenset() принимает список (или любую другую коллекцию) и строит из него неизменяемое множество.
  • print(fs) — выведет frozenset({'read', 'write'}): Python явно показывает, что это именно frozenset, а не обычное множество.
  • fs2 = frozenset(["write", "delete"]) — ещё один frozenset.
  • fs & fs2 — операции с множествами (&, |, -, ^) у frozenset работают точно так же, как у обычного: они не меняют исходный frozenset, а создают новый.

Попытка изменить frozenset — например, fs.add("delete") — вызовет ошибку AttributeError, потому что у frozenset такого метода попросту нет: неизменяемость гарантирована на уровне самого типа.

Когда пригодится: когда данные по смыслу не должны меняться (например, фиксированный набор прав доступа), либо когда обычный set использовать нельзя — например, как ключ словаря. У обычного множества такого права нет именно потому, что оно изменяемое.

Практика: убрать дубликаты и найти совпадения

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

Убрать повторы из списка одной строкой

votes = ["Алиса", "Боб", "Алиса", "Вера", "Боб"]
unique_voters = list(set(votes))
print(unique_voters)
print(f"Уникальных голосующих: {len(unique_voters)}")
  • votes = [...] — список с именами, некоторые повторяются.
  • list(set(votes)) — сначала set(votes) строит множество из списка (повторы пропадают), затем list(...) оборачивает результат обратно в список, ведь дальше со списком удобнее работать — например, обращаться по индексу.
  • print(unique_voters) — выведет список из четырёх уникальных имён, но в произвольном порядке: ['Боб', 'Вера', 'Алиса'] (порядок мог быть и другим).
  • print(f"Уникальных голосующих: {len(unique_voters)}") — len() считает длину списка, а f-строка подставляет число прямо в текст. Выведет Уникальных голосующих: 3.

Найти общее и разное между двумя наборами

required_list = ["python", "sql", "git", "docker"]
my_list = ["python", "git", "linux"]

required = set(required_list)
have = set(my_list)

match = required & have
missing = required - have

print(f"Совпадают: {match}")
print(f"Не хватает: {missing}")
  • required = set(required_list), have = set(my_list) — превращаем оба списка в множества, чтобы можно было применить операции пересечения и разности.
  • match = required & have — пересечение: то, что нужно и уже есть. Получится {'python', 'git'}.
  • missing = required - have — разность: то, что нужно, но ещё не освоено. Получится {'sql', 'docker'}.
  • Обе f-строки подставляют получившиеся множества прямо в текст вывода.

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

Ошибка 1. Пустое множество через {}

seen = {}
seen.add("a")

Здесь будет AttributeError: 'dict' object has no attribute 'add', потому что {} создал словарь, а у словаря нет метода add. Правильно: seen = set().

Ошибка 2. Обращение по индексу

colors = {"red", "green"}
first = colors[0]

TypeError: 'set' object is not subscriptable — у множества нет пронумерованных мест. Если нужен порядок и доступ по номеру — используйте список.

Ошибка 3. remove() на несуществующем элементе

colors = {"red", "green"}
colors.remove("blue")

KeyError: 'blue' — элемента "blue" в множестве не было, а remove() в такой ситуации бросает ошибку. Если не уверены, что элемент точно есть — используйте discard(), он не упадёт.

Что важно запомнить

  • {} — это словарь, не множество. Пустое множество: только set()
  • Множество хранит только уникальные элементы — дубли исчезают автоматически
  • У множества нет индексов и нет гарантий порядка
  • add() добавляет один элемент, update() — сразу несколько
  • remove() бросает KeyError на отсутствующем элементе, discard() молчит
  • Операции: | объединение, & пересечение, - разность, ^ симметричная разность
  • issubset() проверяет, что все элементы одного множества есть в другом
  • frozenset — неизменяемая версия множества
  • list(set(lst)) — быстрый способ убрать дубликаты из списка (без сохранения порядка)
  • Проверка x in множество выполняется практически мгновенно, в отличие от списка

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

5 вопросов

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

Premium

Уникальные элементы

Premium

Есть ли повторы в строке

Дана строка word. Определите, есть ли в ней повторяющиеся буквы (какая-то буква встречается больше одного раза).

Результат запишите в переменную has_duplicates: True, если повтор есть, False, если все буквы в слове разные.

Пример: для строки "привет" все буквы разные — has_duplicates должен быть False. Для строки "яблоко" буква «о» встречается дважды — has_duplicates должен быть True.