Примеры идут от простого к сложному: сначала 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() <= не сработает.
Что делать дальше
Посмотрите файлы репозитория — там те же идеи в исходном виде из лекции, затем переходите к заданиям. Если что-то не запустилось — загляните в типичные ошибки.