💻 Примеры

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

user = {"name": "Alice", "age": 30}
user["city"] = "NY"                 # добавить
user.update(age=31)                 # обновить существующий ключ
age = user.pop("age")               # удалить и получить значение
print(user.get("city"))             # NY  — без риска KeyError
frozen = frozenset(["read", "write"])
lookup = {frozen: "editor"}         # frozenset — хешируемый ключ
ОперацияЧто делаетРиск
d[key]получить значениеKeyError, если ключа нет
d.get(key, default)получить безопаснонет исключений
d.update(...)добавить/перезаписать ключитихо затирает старое значение
d.pop(key) / del d[key]удалить (с возвратом / без)KeyError без значения по умолчанию
frozenset(...)неизменяемый наборнет add()/remove()
Топ-3 ошибки: {} — это пустой словарь, а не множество (для пустого множества нужен set()) · список или обычный set как ключ словаря → TypeError: unhashable type · "значение" in dict проверяет ключи, а не значения.

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

Пример 1. Set comprehension: та же идея, что list comprehension

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

# set_comprehension.py
words = ["apple", "banana", "cherry", "apple"]
unique_lengths = {len(word) for word in words}
print(unique_lengths)   # {5, 6}

vowels_used = {ch for word in words for ch in word if ch in "aeiou"}
print(sorted(vowels_used))   # ['a', 'e']

Что происходит: четыре слова дают длины 5, 6, 6, 5 — но множество хранит каждое значение один раз, поэтому результат {5, 6}. Второй пример — вложенный comprehension: сначала перебираются слова, затем буквы внутри каждого слова, и в множество попадают только гласные.

Пример 2. frozenset — множество, которое нельзя изменить

frozenset создаётся из любой последовательности и поддерживает операции чтения (|, &, -), но не поддерживает методы, которые меняют содержимое на месте.

# frozenset_immutable.py
immutable_set = frozenset([1, 2, 3, 4, 5])
print(immutable_set)                        # frozenset({1, 2, 3, 4, 5})
print(immutable_set | frozenset([4, 5, 6]))  # frozenset({1, 2, 3, 4, 5, 6})

try:
    immutable_set.add(6)
except AttributeError as error:
    print("AttributeError:", error)
    # 'frozenset' object has no attribute 'add'

Что происходит: | (объединение) работает и возвращает новый frozenset — исходный при этом не меняется. А вот .add() вообще отсутствует как метод у типа: это не поведение времени выполнения, а гарантия на уровне класса.

Пример 3. frozenset как ключ словаря или элемент множества

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

# frozenset_as_key.py
permissions = {
    frozenset(["read"]): "viewer",
    frozenset(["read", "write"]): "editor",
}
current_permissions = frozenset(["write", "read"])
print(permissions[current_permissions])   # editor

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

Что происходит: порядок элементов внутри frozenset не важен — frozenset(["write", "read"]) находит тот же ключ, что frozenset(["read", "write"]), потому что оба множества равны по содержимому. Поэтому во втором выражении из трёх frozenset остаётся только два уникальных.

Пример 4. Создание словаря и требование к хешируемости ключа

dict() строит словарь из именованных аргументов или из последовательности пар «ключ, значение». Ключом может быть любой хешируемый объект: число, строка, кортеж, frozenset — но не список и не обычный set.

# dict_creation.py
person = dict(name="Alice", age=30, city="New York")
print(person)   # {'name': 'Alice', 'age': 30, 'city': 'New York'}

pairs = [("name", "Charlie"), ("age", 35), ("city", "Paris")]
person2 = dict(pairs)
print(person2["name"])   # Charlie

same_key = {1.0: "float", 1: "integer", True: "boolean"}
print(same_key)   # {1.0: 'boolean'}

try:
    bad = {[1, 2]: "list key"}
except TypeError as error:
    print("TypeError:", error)
    # unhashable type: 'list'

Что происходит: 1, 1.0 и True равны друг другу и хешируются одинаково, поэтому все три присваивания попадают в один ключ — в словаре остаётся запись с ключом 1.0 (по первому вхождению) и последним присвоенным значением 'boolean'. Список как ключ падает сразу же, ещё до обращения к словарю — потому что список изменяемый.

Пример 5. Доступ по ключу: [], get() и in

Квадратные скобки — самый строгий способ достать значение: если ключа нет, программа падает с KeyError. get() и in — безопасные альтернативы.

# access_by_key.py
person = {"name": "Alice", "age": 30}
try:
    print(person["city"])
except KeyError as error:
    print("KeyError:", error)   # 'city'

print(person.get("city"))              # None
print(person.get("city", "не указан")) # не указан
print("city" in person)                # False

if "name" in person:
    print(person["name"])   # Alice

Что происходит: get() не бросает исключение — вместо этого возвращает None или явно заданное второе значение. Паттерн «проверить in, затем прочитать []» безопасен, но требует двух обращений к словарю; на практике чаще выбирают одно обращение через get().

Пример 6. Добавление и обновление: [] = и update()

# add_and_update.py
user = {"name": "Alice", "age": 30}
user["city"] = "New York"                       # новый ключ — добавлен
user.update({"age": 32, "country": "USA"})       # существующий — перезаписан
print(user)
# {'name': 'Alice', 'age': 32, 'city': 'New York', 'country': 'USA'}

user.update(age=33, phone="000")   # update() тоже принимает именованные аргументы
print(user)
# {'name': 'Alice', 'age': 33, 'city': 'New York', 'country': 'USA', 'phone': '000'}

Что происходит: присваивание по [] и update() ведут себя одинаково по сути — если ключ новый, он добавляется; если существующий, старое значение молча заменяется новым. update() удобен, когда нужно поменять сразу несколько пар за один вызов.

Пример 7. Удаление: del, pop(), popitem(), setdefault()

# remove_from_dict.py
user = {"name": "Alice", "age": 30, "city": "New York"}
age = user.pop("age")
print(age, user)   # 30 {'name': 'Alice', 'city': 'New York'}

del user["city"]
print(user)   # {'name': 'Alice'}

d = {"a": 1, "b": 2, "c": 3}
print(d.popitem())   # ('c', 3) — в современном Python удаляется последняя добавленная пара
print(d)              # {'a': 1, 'b': 2}

d2 = {"x": 1}
d2.setdefault("x", 100)   # ключ уже есть — значение не меняется
d2.setdefault("y", 200)   # ключа нет — добавляется со значением по умолчанию
print(d2)   # {'x': 1, 'y': 200}

Что происходит: pop() удаляет и возвращает значение — удобно, когда значение сразу нужно использовать. del просто удаляет, ничего не возвращая. setdefault(key, default) — «добавь, если ключа ещё нет» одной строкой вместо if key not in d: d[key] = default.

Пример 8. Инверсия словаря и группировка по ключу

Две частые задачи на практике: поменять местами ключи и значения, и сгруппировать записи по общему признаку — здесь признак сам по себе неизменяемый набор (frozenset), поэтому годится в ключ.

# invert_and_group.py
original_dict = {"a": 1, "b": 2, "c": 3}
inverted_dict = {v: k for k, v in original_dict.items()}
print(inverted_dict)   # {1: 'a', 2: 'b', 3: 'c'}

users = {
    "alice": ["read", "write"],
    "bob": ["write", "read"],
    "charlie": ["read"],
}
groups = {}
for user_name, perms in users.items():
    key = frozenset(perms)
    groups.setdefault(key, []).append(user_name)
print(groups)
# {frozenset({'write', 'read'}): ['alice', 'bob'], frozenset({'read'}): ['charlie']}
# — порядок элементов внутри frozenset не гарантирован, у вас может быть другим

Что происходит: инверсия через dict comprehension короче ручного цикла из теории (for key in original_dict: inverted_dict[original_dict[key]] = key) — оба варианта делают одно и то же, но comprehension компактнее. В группировке alice и bob попадают в одну группу, потому что frozenset(["read", "write"]) и frozenset(["write", "read"]) — один и тот же ключ (порядок печати элементов внутри самого frozenset при этом не фиксирован).

Пример 9. Перебор словаря: ключи, значения, пары, сортировка

# iterate_dict.py
prices = {"banana": 30, "apple": 80, "cherry": 200}

for k in prices:                # перебор словаря даёт ключи
    print(k, end=" ")
print()   # banana apple cherry

print(list(prices.keys()))      # ['banana', 'apple', 'cherry']
print(list(prices.values()))    # [30, 80, 200]

for name, price in sorted(prices.items(), key=lambda item: item[1]):
    print(name, price)
# banana 30
# apple 80
# cherry 200

Что происходит: обычный for k in prices перебирает именно ключи — про это часто забывают и пишут лишний .keys(). Сортировка по значению требует key= у sorted() — эта техника подробно разбирается в уроке 42 «Lambda-функции».

Пример 10. dict как множество: сравнение и общие ключи

Ключи словаря ведут себя как множество — их можно сравнивать операторами множеств. items() в некоторых случаях тоже поддерживает такие операции, если значения хешируемы.

# dict_as_set.py
d1 = {"a": 1, "b": 2}
d2 = {"a": 1, "b": 2, "c": 3}

print(all(item in d2.items() for item in d1.items()))   # True — способ из теории
print(set(d1) <= set(d2))                                # True — через set()
print(d1.items() <= d2.items())                          # True — напрямую, короче

common = d1.keys() & d2.keys()
print(sorted(common))   # ['a', 'b']
⚠️ Проверить по документации: то, что dict.keys() и dict.items() — «view»-объекты, поддерживающие операции множеств (&, |, <=), не входит в конспект урока. Сверьтесь с разделом Dictionary view objects.

Что происходит: все три строки проверяют одно и то же — является ли d1 «подмножеством» d2 по парам ключ-значение, но d1.items() <= d2.items() короче ручного all(...) из теории. Такое поведение доступно только когда все значения хешируемы (числа, строки) — со списками в значениях .items() <= не сработает.

Что делать дальше

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