Примеры идут от простого к сложному: сначала способы создать словарь, затем get() и setdefault() в сравнении, fromkeys() и его главная ловушка, сравнение словарей и объединение, и в конце - живые view objects и defaultdict как альтернатива setdefault(). Весь вывод в комментариях получен реальным запуском кода.
Пример 1. Четыре способа создать словарь - один результат
Один и тот же словарь можно собрать литералом, функцией dict(), из пар zip() или через dict comprehension. Выбор способа зависит от того, откуда пришли данные.
d_literal = {"a": 1, "b": 2, "c": 3}
d_call = dict(a=1, b=2, c=3)
d_pairs = dict([("a", 1), ("b", 2), ("c", 3)])
keys = ["a", "b", "c"]
values = [1, 2, 3]
d_zip = dict(zip(keys, values))
d_comp = {x: x ** 2 for x in range(6) if x % 2 == 0}
print(d_literal) # {'a': 1, 'b': 2, 'c': 3}
print(d_call) # {'a': 1, 'b': 2, 'c': 3}
print(d_pairs) # {'a': 1, 'b': 2, 'c': 3}
print(d_zip) # {'a': 1, 'b': 2, 'c': 3}
print(d_comp) # {0: 0, 2: 4, 4: 16}
print(d_literal == d_call == d_pairs) # True - все три способа дали одинаковый словарь
Что происходит: dict(a=1, ...) работает только если ключи - корректные имена переменных; dict([...]) и dict(zip(...)) принимают любую итерируемую пару «ключ, значение»; comprehension умеет сразу и вычислять значение, и фильтровать по условию.
Пример 2. get(): безопасное чтение без KeyError
dictionary.get(key, default) возвращает значение по ключу или default (по умолчанию None), если ключа нет - и никогда не меняет сам словарь.
user = {"name": "Alice", "age": 30}
print(user.get("age")) # 30
print(user.get("city")) # None - ключа нет, но ошибки нет
print(user.get("city", "Unknown")) # Unknown - свой default вместо None
print(user) # {'name': 'Alice', 'age': 30} - словарь не изменился
Что происходит: третий вызов не создаёт ключ "city" в user - get() только читает и возвращает default, ничего не записывая обратно в словарь.
Пример 3. setdefault(): тот же вопрос, но словарь меняется
setdefault(key, default) внешне похож на get(), но если ключа не было - он добавляет пару key: default прямо в словарь. Это главное отличие двух методов.
d = {"a": 1, "b": 2}
print(d.setdefault("a")) # 1 - ключ уже был, вернул его значение
print(d.setdefault("c")) # None - ключа не было, создал c=None
print(d.setdefault("d", 0)) # 0 - ключа не было, создал d=0
print(d) # {'a': 1, 'b': 2, 'c': None, 'd': 0} - словарь ИЗМЕНИЛСЯ
Что происходит: после трёх вызовов в d появились два новых ключа - c и d, хотя код лишь «спрашивал» значения. Если словарь менять нельзя, setdefault() - не тот метод.
Пример 4. get() и setdefault() дают одинаковый счётчик
Классическая задача - посчитать, сколько раз встречается каждый символ в строке. Оба метода решают её одинаково коротко и дают идентичный результат.
text = "Что говорить, когда нечего говорить?".lower()
counter_get = {}
for char in text:
counter_get[char] = counter_get.get(char, 0) + 1
counter_set = {}
for char in text:
counter_set[char] = counter_set.setdefault(char, 0) + 1
print(counter_get)
# {'ч': 2, 'т': 3, 'о': 7, ' ': 4, 'г': 4, 'в': 2, 'р': 2, 'и': 2,
# 'ь': 2, ',': 1, 'к': 1, 'д': 1, 'а': 1, 'н': 1, 'е': 2, '?': 1}
print(counter_get == counter_set) # True - оба варианта совпали
Что происходит: для значений-чисел (immutable) оба метода равноправны, а counter_get[char] = counter_get.get(char, 0) + 1 обычно читают чуть проще - следующий пример покажет случай, где это уже не так.
Пример 5. Группировка списком: здесь setdefault() выигрывает
Как только значением становится изменяемый объект (список), с get() приходится писать две строки, а setdefault() справляется одной - потому что возвращает сам список и сразу позволяет вызвать .append().
words = ["кот", "корова", "собака", "слон", "кошка", "сорока"]
grouped_setdefault = {}
for word in words:
grouped_setdefault.setdefault(word[0], []).append(word)
grouped_get = {}
for word in words:
key = word[0]
grouped_get[key] = grouped_get.get(key, [])
# grouped_get.get(key, []).append(word) не сработает -
# append() возвращает None, а не список
grouped_get[key].append(word)
print(grouped_setdefault)
# {'к': ['кот', 'корова', 'кошка'], 'с': ['собака', 'слон', 'сорока']}
print(grouped_get == grouped_setdefault) # True - результат один и тот же
Что происходит: для mutable-значений (список, словарь, множество) удобнее setdefault(); для immutable-значений (числа, строки) обычно удобнее get() - так советует и источник урока.
Пример 6. dict.fromkeys(): словарь с одинаковым значением
dict.fromkeys(iterable, value) создаёт словарь, где все ключи из iterable получают одно и то же value (по умолчанию None). Удобно для инициализации.
letters = ["x", "y", "z"]
print(dict.fromkeys(letters)) # {'x': None, 'y': None, 'z': None}
print(dict.fromkeys(letters, 0)) # {'x': 0, 'y': 0, 'z': 0}
Что происходит: оба вызова не трогают letters - fromkeys() берёт ключи из итерируемого объекта и строит новый словарь.
Пример 7. Ловушка fromkeys(): один список на всех
Если передать в value изменяемый объект, все ключи будут ссылаться на один и тот же объект - это самая частая ошибка с fromkeys().
keys = ["a", "b", "c"]
bad = dict.fromkeys(keys, [])
bad["a"].append(1)
print(bad) # {'a': [1], 'b': [1], 'c': [1]} - "потянулись" все три!
print(bad["a"] is bad["b"]) # True - это буквально один и тот же список
good = {key: [] for key in keys}
good["a"].append(1)
print(good) # {'a': [1], 'b': [], 'c': []} - списки независимые
Что происходит: fromkeys(keys, []) кладёт в каждый ключ ссылку на один и тот же список, поэтому изменение через любой ключ видно у всех. Исправление - dict comprehension {key: [] for key in keys}: тело выполняется отдельно для каждого ключа и создаёт новый список каждый раз.
Пример 8. Сравнение словарей: содержимое, а не порядок
Оператор == сравнивает словари как множества пар «ключ-значение» - порядок добавления ключей не важен, а вот совпадение типов значений важно.
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
d3 = {"b": 2, "a": "1"}
print(d1 == d2) # True - содержимое одинаковое, порядок ключей другой
print(d1 == d3) # False - значение 1 (int) != '1' (str)
print(d1 != d3) # True
e1 = {"a": 1, "b": 2, "c": 3}
e2 = {"b": 20, "c": 3, "d": 4}
print(sorted(e1.keys() & e2.keys())) # ['b', 'c'] - общие ключи
print(sorted(e1.keys() - e2.keys())) # ['a'] - есть только в e1
print(sorted(e1.keys() ^ e2.keys())) # ['a', 'd'] - есть только в одном из двух
print(sorted(e1.keys() | e2.keys())) # ['a', 'b', 'c', 'd'] - объединение ключей
Что происходит: d1 == d3 ложно из-за разницы типов значения (1 против "1"), хотя ключи те же. А вот сами ключи (не словари целиком!) поддерживают все операции множеств - &, -, ^, |; результат каждой из них - обычное множество, поэтому для предсказуемого вывода его оборачивают в sorted() (как уже делали в уроке 32) - без этого порядок печати элементов не гарантирован.
Пример 9. Объединение словарей: | и update()
С Python 3.9 словари можно объединять оператором | - он возвращает новый словарь. Метод update() делает то же самое, но меняет словарь на месте. В обоих случаях при конфликте ключей побеждает «правый» источник.
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
merged = a | b
print(merged) # {'x': 1, 'y': 20, 'z': 3} - справа побеждает при конфликте
print(a) # {'x': 1, 'y': 2} - a не изменился, | создаёт НОВЫЙ словарь
c = {"x": 1, "y": 2}
c.update(b)
print(c) # {'x': 1, 'y': 20, 'z': 3} - update() меняет c НА МЕСТЕ
| для словарей появился в Python 3.9 (PEP 584) - в лекции упомянуто только объединение как таковое. Сверьтесь с docs.python.org/3/library/stdtypes.html#dict.
Пример 10. keys(), values(), items() - живые view objects
Эти три метода возвращают не список, а «представление» (view object), которое связано со словарём и сразу отражает его изменения - в отличие от снимка, сделанного через list().
profile = {"name": "Alice", "age": 30}
keys_view = profile.keys()
print(keys_view) # dict_keys(['name', 'age'])
profile["city"] = "New York"
print(keys_view) # dict_keys(['name', 'age', 'city']) - view "увидел" новый ключ
for key, value in profile.items():
print(key, "->", value)
# name -> Alice
# age -> 30
# city -> New York
snapshot = list(profile.keys())
profile["country"] = "USA"
print(snapshot) # ['name', 'age', 'city'] - список - это снимок, а не view
Что происходит: keys_view не хранит копию ключей, а каждый раз смотрит в актуальный словарь - поэтому после добавления "city" он «подрос» сам. А вот list(profile.keys()) - обычный список, зафиксированный в момент вызова: следующее изменение profile на него уже не влияет.
Пример 11. defaultdict как альтернатива setdefault()
Когда группировка через setdefault(key, []).append(...) повторяется во многих местах кода, collections.defaultdict убирает default прямо из вызова - словарь сам создаёт значение при первом обращении к новому ключу.
from collections import defaultdict
groups = defaultdict(list)
for word in words:
groups[word[0]].append(word)
print(groups)
# defaultdict(<class 'list'>, {'к': ['кот', 'корова', 'кошка'], 'с': ['собака', 'слон', 'сорока']})
print(dict(groups))
# {'к': ['кот', 'корова', 'кошка'], 'с': ['собака', 'слон', 'сорока']}
defaultdict не входит в материал этой лекции - это дополнение курса, взятое из старого-vs-нового урока. Полный разбор - в уроке 40 «Модуль collections»; синтаксис сверяйте по docs.python.org/3/library/collections.html#collections.defaultdict.
Что делать дальше
Разберите файлы репозитория - там те же идеи в исходном виде из лекции, затем переходите к заданиям. Если что-то из примеров не запустилось - загляните в типичные ошибки. Базовое создание словарей и KeyError разобраны в уроке 34, а закрепление всех приёмов ждёт в уроке 37 «Практикум 9».