Словарь в Python: ключи, значения и методы dict

Python Автор: Среда и версия: CPython 3.14.5
содержание

Словарь хранит пары «ключ и значение» и находит значение по ключу мгновенно, сколько бы записей в нём ни лежало. Список для этого пришлось бы перебирать целиком. Ключ обязан быть хешируемым, значением может быть что угодно, включая другой словарь. Весь код ниже выполнен на CPython 3.14.5.

Python / 01

Ключ ведёт прямо к значению

Ключ ведёт прямо к значению01 / вход 02 / операция 03 / результат хлеб prices["хлеб"] 60 молоко prices["молоко"] 90 сыр prices["сыр"] 40001 / вход02 / операция03 / результатхлебprices["хлеб"]60молокоprices["молоко"]90сырprices["сыр"]400
В квадратных скобках указывают ключ, а не номер позиции: 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. Проверить себя на живой задаче можно в дедупликации событий: там пригодится ровно то свойство словаря, ради которого его и берут, мгновенная проверка «уже встречалось».

Источники