💻 Примеры

⚡ get, setdefault, fromkeys и сравнение словарей

d = dict(zip(["a", "b"], [1, 2]))
print(d.get("c", 0))          # 0 - словарь не изменился
print(d.setdefault("c", 0))   # 0 - и добавил "c" в d
print(d)                      # {'a': 1, 'b': 2, 'c': 0}
print(dict.fromkeys(["x", "y"], 0))  # {'x': 0, 'y': 0}
print({"a": 1, "b": 2} == {"b": 2, "a": 1})  # True
print(d | {"b": 20})          # {'a': 1, 'b': 20, 'c': 0}
КонструкцияЧто делаетМини-пример
dictionary.get(key, default)читает безопасно, словарь НЕ меняетd.get("c", 0)
dictionary.setdefault(key, default)читает и, если ключа нет, ДОБАВЛЯЕТ егоd.setdefault("c", 0)
dict.fromkeys(keys, value)создаёт словарь с одним value для всех ключейdict.fromkeys(["x", "y"], 0)
d1 == d2сравнивает по содержимому, порядок ключей не важен{"a": 1} == {"a": 1}
d1 | d2 / d1.update(d2)объединяет словари; при конфликте побеждает правыйd1 | d2
Топ-3 ошибки: fromkeys(keys, []) — один список на всех ключах · setdefault() добавляет ключ незаметно · d1 == d2 сравнивает содержимое, не порядок.

Примеры идут от простого к сложному: сначала способы создать словарь, затем 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».