Словарь хранит пары «ключ и значение» и находит значение по ключу мгновенно, сколько бы записей в нём ни лежало. Список для этого пришлось бы перебирать целиком. Ключ обязан быть хешируемым, значением может быть что угодно, включая другой словарь. Весь код ниже выполнен на CPython 3.14.5.
Ключ ведёт прямо к значению
prices["молоко"] возвращает 90.Как создать словарь и получить значение
Фигурные скобки, пары через двоеточие, запятая между парами. Значение берётся по ключу в квадратных скобках:
prices = {"хлеб": 60, "молоко": 90, "сыр": 400}
print(prices["молоко"])
print(len(prices))
90
3
Пустой словарь — это {}, а set() создаёт пустое множество: одни и те же скобки, разный результат. Есть ещё три способа собрать словарь, и все дают одно и то же:
a = {"хлеб": 60, "молоко": 90}
b = dict(хлеб=60, молоко=90)
c = dict([("хлеб", 60), ("молоко", 90)])
print(a == b == c)
print(dict(zip(["хлеб", "молоко"], [60, 90])))
True
{'хлеб': 60, 'молоко': 90}
Форма с zip полезна, когда ключи и значения приходят двумя отдельными списками. Например, заголовки таблицы и строка данных.
Почему возникает KeyError и как его избежать
Обращение к несуществующему ключу через квадратные скобки роняет программу, и падение приходится перехватывать:
prices = {"хлеб": 60, "молоко": 90, "сыр": 400}
try:
print(prices["масло"])
except KeyError as exc:
print(f"KeyError: {exc}")
KeyError: 'масло'
В сообщении стоит сам ключ, и это первое, на что надо смотреть: чаще всего там опечатка, лишний пробел или другой регистр. Метод get вместо падения возвращает None, а со вторым аргументом отдаёт указанное значение:
prices = {"хлеб": 60, "молоко": 90, "сыр": 400}
print(prices.get("масло"))
print(prices.get("масло", 0))
print(prices.get("сыр", 0))
None
0
400
Выбор между [] и get содержательный, а не стилистический. Квадратные скобки говорят «ключ обязан быть, иначе это баг», get говорит «ключа может не быть, и это нормально». Ставить get везде подряд вредно: тогда опечатка в имени ключа не упадёт, а тихо подставит значение по умолчанию.
Проверить наличие ключа отдельно умеет оператор in. Важно, что он смотрит именно на ключи:
prices = {"хлеб": 60, "молоко": 90, "сыр": 400}
print("сыр" in prices, 400 in prices, 400 in prices.values())
True False True
Число 400 лежит в словаре значением, поэтому 400 in prices даёт False, а 400 in prices.values() даёт True. Поиск по значениям при этом идёт перебором и на больших словарях становится дорогим.
Как добавить, обновить и объединить
Добавление и обновление пишутся одинаково: если ключа нет, он появится, если есть, значение заменится.
d = {"хлеб": 60}
d["молоко"] = 90
d["хлеб"] = 65
print(d)
d.update({"сыр": 400, "хлеб": 70})
print(d)
{'хлеб': 65, 'молоко': 90}
{'хлеб': 70, 'молоко': 90, 'сыр': 400}
Отдельного метода add у словаря нет, и искать его не нужно. update делает то же самое пачкой и точно так же перезаписывает совпавшие ключи.
Начиная с Python 3.9 два словаря объединяются оператором |, который возвращает новый словарь и не трогает исходные:
base = {"хлеб": 60, "молоко": 90}
sale = {"молоко": 75, "сыр": 400}
print(base | sale)
print(sale | base)
print(base)
{'хлеб': 60, 'молоко': 75, 'сыр': 400}
{'молоко': 90, 'сыр': 400, 'хлеб': 60}
{'хлеб': 60, 'молоко': 90}
Порядок операндов решает исход конфликта: побеждает значение правого словаря. В первой строке молоко стоит 75 из распродажи, во второй — 90 из базового прайса. Форма base |= sale меняет левый словарь на месте, как update.
Когда значение надо создать только при первом обращении, помогает setdefault:
counts = {}
for word in ["да", "нет", "да"]:
counts.setdefault(word, 0)
counts[word] += 1
print(counts)
{'да': 2, 'нет': 1}
Он кладёт значение по умолчанию, если ключа нет, и ничего не делает, если ключ уже есть. Для подсчётов эту работу лучше отдать Counter, а для накопления списков — defaultdict, оба показаны в конце.
Как удалить элемент
Три способа, и различаются они тем, что происходит с удалённым значением и с отсутствующим ключом.
d = {"хлеб": 60, "молоко": 90, "сыр": 400}
del d["хлеб"]
print(d)
print(d.pop("молоко"), d)
print(d.pop("масло", "нет такого"))
try:
d.pop("масло")
except KeyError as exc:
print(f"KeyError: {exc}")
{'молоко': 90, 'сыр': 400}
90 {'сыр': 400}
нет такого
KeyError: 'масло'
del просто удаляет и падает на отсутствующем ключе. pop возвращает удалённое значение и умеет не падать, если дать ему второй аргумент. Метод popitem снимает последнюю добавленную пару и возвращает её кортежем, а clear опустошает словарь целиком.
d = {"a": 1, "b": 2, "c": 3}
print(d.popitem(), d)
('c', 3) {'a': 1, 'b': 2}
keys, values и items — это живые представления
Три метода возвращают не списки, а представления: окна в словарь, которые видят его текущее состояние.
prices = {"хлеб": 60, "молоко": 90}
keys = prices.keys()
print(keys)
prices["сыр"] = 400
print(keys)
print(list(prices.values()), list(prices.items()))
dict_keys(['хлеб', 'молоко'])
dict_keys(['хлеб', 'молоко', 'сыр'])
[60, 90, 400] [('хлеб', 60), ('молоко', 90), ('сыр', 400)]
Переменная keys создана до добавления сыра, а показывает его. Так и задумано: представление не копирует данные, а смотрит на них. Из-за этого обращение по индексу не работает:
prices = {"хлеб": 60, "молоко": 90}
try:
print(prices.keys()[0])
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: 'dict_keys' object is not subscriptable
Нужен список, оберни в list(). Кстати, list(prices) без метода тоже даёт ключи, а sorted(prices) даёт их же по алфавиту:
prices = {"хлеб": 60, "молоко": 90, "сыр": 400}
print(list(prices), sorted(prices))
['хлеб', 'молоко', 'сыр'] ['молоко', 'сыр', 'хлеб']
Перебор словаря циклом идёт по ключам, а пары достаёт .items().
Как работать со списком словарей
Список словарей — обычная форма данных из API и из выгрузок: строка таблицы становится словарём, таблица целиком становится списком. Даты в таком ответе приходят строками, и превращает их в объекты модуль datetime.
users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
]
print([u["name"] for u in users if u["city"] == "Москва"])
print(sorted(users, key=lambda u: u["orders"], reverse=True)[0]["name"])
print(sum(u["orders"] for u in users))
['Аня', 'Вера']
Аня
5
Фильтр — это списковое включение с условием. Сортировка идёт через key, где функция достаёт из словаря поле, по которому сравнивать. Сумма считается по генератору без промежуточного списка.
Группировка по полю — самая частая операция над такими данными, и делается она словарём, где значение это список:
users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
]
by_city = {}
for u in users:
by_city.setdefault(u["city"], []).append(u["name"])
print(by_city)
{'Москва': ['Аня', 'Вера'], 'Казань': ['Борис']}
Тот же результат короче даёт defaultdict(list): он сам создаёт пустой список при первом обращении к новому ключу, и setdefault становится не нужен.
Сохраняется ли порядок ключей
Да, с Python 3.7 порядок вставки гарантирован языком, а не деталями реализации:
d = {}
for k in ["я", "б", "а"]:
d[k] = 1
print(list(d))
['я', 'б', 'а']
Ключи идут в том порядке, в каком их положили, без всякой сортировки. При этом сравнение словарей от порядка не зависит вовсе:
print({"a": 1, "b": 2} == {"b": 2, "a": 1})
True
Два словаря равны, когда совпадают наборы пар. Это отличает их от списков, где [1, 2] != [2, 1].
Как скопировать словарь и не сломать оригинал
Присваивание копию не создаёт: два имени начинают указывать на один и тот же словарь. Метод copy() создаёт новый словарь, но вложенные объекты остаются общими:
original = {"user": {"name": "Аня"}, "tags": ["новый"]}
shallow = original.copy()
shallow["tags"].append("вип")
shallow["user"]["name"] = "Анна"
print(original)
{'user': {'name': 'Анна'}, 'tags': ['новый', 'вип']}
Оригинал изменился, хотя правили копию. Верхний уровень действительно скопирован, а список и вложенный словарь в обоих объектах одни и те же. Полностью независимую копию делает deepcopy:
import copy
original = {"user": {"name": "Аня"}, "tags": ["новый"]}
deep = copy.deepcopy(original)
deep["tags"].append("вип")
deep["user"]["name"] = "Анна"
print(original)
{'user': {'name': 'Аня'}, 'tags': ['новый']}
Оригинал цел. За глубокое копирование платят временем и памятью, поэтому его берут осознанно, а не по умолчанию. Почему вообще возникает такая разница, разобрано в статье про изменяемые и неизменяемые типы.
Ошибки, на которых спотыкаются
dict.fromkeys с изменяемым значением. Выглядит как удобный способ создать словарь с пустыми списками, а создаёт один список на всех:
shared = dict.fromkeys(["a", "b"], [])
shared["a"].append(1)
print(shared)
{'a': [1], 'b': [1]}
Единица попала в оба ключа. Значение по умолчанию вычисляется один раз, и обе записи ссылаются на один объект. Включение создаёт новый список для каждого ключа:
ok = {k: [] for k in ["a", "b"]}
ok["a"].append(1)
print(ok)
{'a': [1], 'b': []}
Цепочка get на вложенных данных. Первый get вернул None, и следующий вызов бьётся об него:
data = {"user": {"profile": {"city": "Москва"}}}
print(data.get("order", {}).get("id", "нет заказа"))
try:
print(data.get("order").get("id"))
except AttributeError as exc:
print(f"AttributeError: {exc}")
нет заказа
AttributeError: 'NoneType' object has no attribute 'get'
Лечится вторым аргументом: get("order", {}) возвращает пустой словарь вместо None, и цепочка продолжается. Само сообщение 'NoneType' object has no attribute — самая частая формулировка AttributeError, и почти всегда за ней стоит не опечатка в имени метода, а None, пришедший откуда-то раньше.
Добавление ключей во время обхода. Менять значения существующих ключей можно:
prices = {"хлеб": 60, "молоко": 90}
for key in prices:
prices[key] = prices[key] * 2
print(prices)
{'хлеб': 120, 'молоко': 180}
А вот добавить или удалить ключ прямо в цикле нельзя:
prices = {"хлеб": 60, "молоко": 90}
try:
for key in prices:
prices[key + "_копия"] = 0
except RuntimeError as exc:
print(f"RuntimeError: {exc}")
RuntimeError: dictionary changed size during iteration
Разница в том, что при замене значения размер словаря не меняется и обход остаётся корректным. Собирай новые ключи в отдельный словарь и объединяй после цикла.
Готовые словари из collections
Два типа из стандартной библиотеки закрывают половину задач, ради которых пишут setdefault.
from collections import Counter, defaultdict
words = ["да", "нет", "да", "да", "нет"]
print(Counter(words))
print(Counter(words).most_common(1))
grouped = defaultdict(list)
for name, city in [("Аня", "Москва"), ("Борис", "Казань"), ("Вера", "Москва")]:
grouped[city].append(name)
print(dict(grouped))
Counter({'да': 3, 'нет': 2})
[('да', 3)]
{'Москва': ['Аня', 'Вера'], 'Казань': ['Борис']}
Counter считает вхождения за один проход и умеет отдавать самые частые элементы. defaultdict(list) вызывает фабрику при каждом обращении к новому ключу, поэтому списки получаются отдельные, в отличие от ловушки с fromkeys выше.
Частые вопросы
Как получить значение по ключу, если ключа может не быть
Методом get: d.get("ключ", значение_по_умолчанию). Квадратные скобки оставляй там, где отсутствие ключа означает ошибку в данных и падение уместно.
Как узнать все ключи и значения словаря
d.keys() даёт ключи, d.values() значения, d.items() пары. Все три возвращают представления, поэтому для индексации и среза их оборачивают в list().
Что может быть ключом словаря
Хешируемый объект: например, строка, число или кортеж из хешируемых элементов. Хеш должен оставаться постоянным, а равные объекты должны иметь одинаковый хеш. Неизменяемость сама по себе этого не гарантирует: кортеж со списком внутри не хешируется. Список и другой словарь тоже не могут быть ключами. Подробно про это в статье про изменяемые и неизменяемые типы.
Как отсортировать словарь по значению
sorted(d.items(), key=lambda pair: pair[1]) даёт список пар по возрастанию значения, reverse=True разворачивает порядок. Сам словарь при этом не сортируется, сортируется его снимок в виде списка.
Чем словарь отличается от списка
Список хранит порядок и доступ по числовому индексу, словарь хранит доступ по произвольному ключу. Поиск нужного элемента в списке требует перебора, в словаре нет.
Что учить дальше
Рядом со словарями стоят списковые и словарные включения, которые собирают словарь одной строкой, и циклы, на которых держится любой обход данных. В пути «Python с нуля» на Koddo под словари отведён свой пак: сначала счётчик слов, потом группировка записей по полю, потом разбор вложенного ответа API. Проверить себя на живой задаче можно в дедупликации событий: там пригодится ровно то свойство словаря, ради которого его и берут, мгновенная проверка «уже встречалось».