💻 Примеры

⚡ set, dict, frozenset на одном экране

nums = {n for n in [3, 4, 5, 6] if n % 3 == 0}   # {3, 6} — set comprehension
lens = {w: len(w) for w in ["a", "bb"]}           # {'a': 1, 'bb': 2} — dict comprehension
common = set(d1) & set(d2)                        # общие ключи двух словарей
groups = {}
groups.setdefault(frozenset(["egg", "milk"]), []).append("Pancakes")  # frozenset как ключ
ИнструментКогда братьОсобенность
setуникальные значения, без порядкаизменяемый → нельзя как ключ словаря
dict comprehensionсписок → словарь{ключ: значение for ...}
frozensetнабор как ключ словарянеизменяемый, поэтому хешируемый
items()сравнить пары ключ-значениеподмножество ≠ совпадение только ключей
Топ-3 ошибки: проверять подмножество словаря только по ключам, забыв про значения · терять рецепты/группы с одинаковым набором элементов, но разным порядком · пытаться использовать обычный set как ключ словаря вместо frozenset.

Практикум закрепляет выбор между set, dict и frozenset на десяти задачах. Примеры идут от отдельных приёмов (comprehension, пересечение множеств, проверка подмножества) к их совмещению в задачах — группировке по неизменяемому набору. Весь вывод в комментариях получен запуском кода.

Пример 1. Задание 1 — числа, кратные 3 или 5

numbers = [16, 18, 1, 6, 3, 2, 6, 2, 14, 3, 20, 15, 19, 4, 18, 15, 15, 4, 20, 18]
result = {number for number in numbers if number % 3 == 0 or number % 5 == 0}
print(result)             # {3, 6, 15, 18, 20}
print(len(numbers), len(result))   # 20 5

Что происходит: set comprehension делает две вещи сразу — фильтрует по условию (% 3 == 0 or % 5 == 0) и убирает дубликаты, потому что set не хранит повторы. В исходном списке 20 чисел с повторами, в результате — 5 уникальных значений, кратных 3 или 5.

Пример 2. Задание 2 — общие ключи двух словарей

dict1 = {"a": 1, "b": 2, "c": 3}
dict2 = {"b": 5, "d": 7, "a": 8}
common_keys = set(dict1) & set(dict2)
print(sorted(common_keys))   # ['a', 'b']
if common_keys:
    print(", ".join(sorted(common_keys)))   # a, b
else:
    print("-")

dict3 = {"x": 1}
common_none = set(dict1) & set(dict3)
print(common_none if common_none else "-")   # -

Что происходит: set(dict1) даёт множество ключей словаря (значения при этом теряются — для поиска общих ключей они и не нужны); & — пересечение множеств. Когда общих ключей нет, set() получается пустым — а пустой set при проверке в if ложен, поэтому нужен явный запасной вариант "-".

Пример 3. Задание 3 — список строк в словарь длин

words = ["apple", "banana", "cherry", "date"]
lengths = {word: len(word) for word in words}
print(lengths)   # {'apple': 5, 'banana': 6, 'cherry': 6, 'date': 4}

Что происходит: dict comprehension {ключ: значение for элемент in коллекция} — прямой аналог list comprehension, но строит пары. Здесь ключом стало само слово, значением — его длина; повторяющихся слов в списке нет, поэтому пар ровно столько же, сколько элементов.

Пример 4. Задание 4 — проверка подмножества словаря

dict_a = {"a": 1, "b": 2}
dict_b = {"a": 1, "b": 2, "c": 3}
is_subset = all(item in dict_b.items() for item in dict_a.items())
print(is_subset)   # True

dict_c = {"a": 1, "b": 99}
is_subset2 = all(item in dict_b.items() for item in dict_c.items())
print(is_subset2)   # False — ключи те же, а значение "b" другое

Что происходит: .items() возвращает пары (ключ, значение), и in сравнивает пары целиком — значит, недостаточно, чтобы ключи dict_a были в dict_b, нужно совпадение и значений (ошибка №1 из топ-3). У dict_c ключи совпадают, но "b": 99 отличается от "b": 2 — проверка справедливо возвращает False.

Пример 5. Задание 5 — удаление пустых значений

data = {"a": None, "b": 2, "c": "", "d": [], "e": [1, 2], "f": 0}
cleaned = {key: value for key, value in data.items() if value}
print(cleaned)   # {'b': 2, 'e': [1, 2]}

Что происходит: if value в comprehension проверяет «истинность» значения — ложны None, "", [] и, что легко упустить, число 0. Здесь "f": 0 тоже удалился вместе с явными пустышками, хотя формально 0 — не то же самое, что «нет значения». Если 0 в вашей задаче значим (например, счётчик), фильтровать нужно явно — if value is not None and value != "", а не полагаться на общую истинность.

Пример 6. Задание 6 — потерянные страницы книги

book = {1: "Начало истории", 2: None, 3: "Глава 1", 4: None, 5: "Глава 2"}
fixed_book = {
    page: (text if text is not None else "Страница потеряна")
    for page, text in book.items()
}
print(fixed_book)
# {1: 'Начало истории', 2: 'Страница потеряна', 3: 'Глава 1', 4: 'Страница потеряна', 5: 'Глава 2'}

Что происходит: здесь проверка — уже не общая истинность, как в примере 5, а точное сравнение is not None. Это важно: текст главы теоретически мог бы быть пустой строкой "" (тоже ложное значение), но задача просит заменить только явное отсутствие данных — None, а не любое пустое значение.

Пример 7. Задание 7 — вложенный словарь оценок

grades = {
    "anna": [5, 4, 3, 5],
    "bennet": [3, 2, 4],
    "john": [5, 5, 5],
}

report = {}
for student, marks in grades.items():
    report[student] = {
        "оценки": marks,
        "средний балл": sum(marks) / len(marks),
    }

print(report)
# {'anna': {'оценки': [5, 4, 3, 5], 'средний балл': 4.25},
#  'bennet': {'оценки': [3, 2, 4], 'средний балл': 3.0},
#  'john': {'оценки': [5, 5, 5], 'средний балл': 5.0}}

Что происходит: значением внешнего словаря стал ещё один словарь — так строится вложенная структура, привычная по уроку 36. Средний балл — обычное sum(marks) / len(marks); для более сложной вложенности (профиль ученика с оценками по нескольким предметам) та же идея просто добавляет ещё один уровень словарей.

Пример 8. Задание 8 — рецепты по набору ингредиентов

recipes = {
    ("flour", "egg", "milk"): "Pancakes",
    ("milk", "flour", "egg"): "Waffles",
    ("sugar", "butter"): "Icing",
}

grouped_recipes = {}
for ingredients, recipe in recipes.items():
    grouped_recipes.setdefault(frozenset(ingredients), []).append(recipe)

print(grouped_recipes[frozenset(["egg", "milk", "flour"])])
# ['Pancakes', 'Waffles']

available = {"egg", "milk", "flour", "sugar"}
for ingredients_needed, recipe_names in grouped_recipes.items():
    if ingredients_needed.issubset(available):
        print(f"Можно приготовить: {recipe_names} из {sorted(ingredients_needed)}")
# Можно приготовить: ['Pancakes', 'Waffles'] из ['egg', 'flour', 'milk']

Что происходит: ключи исходного словаря — кортежи с ингредиентами в разном порядке (("flour", "egg", "milk") и ("milk", "flour", "egg")); если группировать прямо по кортежу, «Pancakes» и «Waffles» попали бы в разные группы, хотя набор один и тот же (ошибка №2 из топ-3). frozenset(ingredients) убирает порядок, и оба рецепта объединяются под одним ключом. Второй фрагмент показывает практическое применение: issubset() находит рецепты, для которых хватает доступных продуктов — «Icing» не попал в вывод, потому что «butter» нет в available.

Пример 9. Задание 9 — студенты с одинаковым набором предметов

students = {
    "Alice": ["Math", "Physics"],
    "Bob": ["Math", "Physics"],
    "Charlie": ["Chemistry", "Biology"],
    "David": ["Math", "Physics"],
    "Eve": ["Chemistry", "Biology"],
}

groups = {}
for student, subjects in students.items():
    groups.setdefault(frozenset(subjects), []).append(student)

for subjects, names in groups.items():
    print(f"Группа с предметами: {', '.join(sorted(subjects))}: {names}")
# Группа с предметами: Math, Physics: ['Alice', 'Bob', 'David']
# Группа с предметами: Biology, Chemistry: ['Charlie', 'Eve']

Что происходит: тот же приём, что и в примере 8 — frozenset как ключ группировки, только источник данных другой (списки предметов у студентов). setdefault(key, []).append(...) экономит проверку «есть ли уже такой ключ» — то же самое, что defaultdict(list) из урока 41, только без импорта.

Пример 10. Задание 10 — перевод слова с предложением добавить новое

english_words = {"cat": "кот", "dog": "собака", "bird": "птица"}

def translate(word, dictionary):
    if word in dictionary:
        return dictionary[word]
    return f"'{word}' нет в словаре — добавьте перевод"


print(translate("cat", english_words))    # кот
print(translate("fish", english_words))   # 'fish' нет в словаре — добавьте перевод

english_words["fish"] = "рыба"
print(translate("fish", english_words))   # рыба

Что происходит: здесь достаточно обычного dict — задача не про уникальность или неизменяемость набора, а про поиск по ключу с понятным сообщением при промахе. После явного добавления пары english_words["fish"] = "рыба" тот же вызов находит перевод — словарь изменяемый, в отличие от frozenset из примеров 8–9.

Пример 11. Почему frozenset, а не set, годится в ключ словаря

try:
    bad = {{"egg", "milk"}: "pancakes"}
except TypeError as error:
    print(f"TypeError: {error}")
# TypeError: unhashable type: 'set'

good = {frozenset(["egg", "milk"]): "pancakes"}
print(good)   # {frozenset({'egg', 'milk'}): 'pancakes'}
# — порядок элементов внутри frozenset не гарантирован, у вас может быть другим
print(frozenset(["egg", "milk"]) == frozenset(["milk", "egg"]))   # True

Что происходит: ключ словаря обязан быть хешируемым, а хешируемость требует неизменяемости — set можно менять после создания (add, remove), поэтому Python запрещает его как ключ (ошибка №3 из топ-3). frozenset — тот же набор уникальных значений, но замороженный: у него нет изменяющих методов, зато есть хеш, и, как показано в примерах 8–9, два frozenset с одинаковыми элементами в разном порядке равны друг другу (хотя порядок печати элементов внутри самого frozenset при этом не фиксирован).

Что дальше

Все десять задач практикума — в заданиях. Если результат разошёлся с ожидаемым — сверьтесь с типичными ошибками и разбором в решениях.