Справочник по Python · модуль 4 · словари, множества и кортежи
Глава 4.2
Словари
в Python
Словарь связывает уникальные ключи со значениями. Вместо позиции items[0] данные находят по смысловому ключу user["name"] — и находят мгновенно, независимо от размера словаря.
- Категория
- Структуры данных
- Уровень
- Базовый → продвинутый
- Связанные темы
- Кортежи, циклы, хеширование, JSON
- Зачем это знать
- Находить и изменять данные по осмысленному ключу
§1Зачем нужен словарь
Данные о человеке можно сложить в список, но обращаться к ним придётся по номерам.
user = ["Max", "admin", 20]
user[1] что здесь? нужно помнить порядок
user = {"name": "Max", "role": "admin", "age": 20}
user["role"] видно без комментариев
У списка две проблемы: порядок полей приходится держать в голове, а поиск нужного значения — например, «найти пользователя с ролью admin» — требует перебора. Словарь решает обе: ключ описывает смысл, а поиск по ключу происходит сразу, без просмотра остальных пар.
§2Пары «ключ → значение»
В списке элемент находят по числовой позиции. В словаре (dict) значение находят по уникальному ключу.
Начиная с Python 3.7 словарь гарантированно сохраняет порядок добавления пар. Но это лишь порядок вывода: обращаться к словарю по числовым позициям всё равно нельзя — data[0] означает поиск ключа 0, а не первого элемента.
§3Создание словаря
user = {"name": "Max", "role": "admin"}
settings = dict(theme="dark", language="ru")
empty = {}
print(user)
print(settings)
print(type(empty))
{'name': 'Max', 'role': 'admin'}
{'theme': 'dark', 'language': 'ru'}
<class 'dict'>Форма dict(theme="dark") короче, но работает только со строковыми ключами, годными в имена: dict(2="x") — синтаксическая ошибка. Пустые фигурные скобки создают именно словарь, а не множество, — про это глава 4.5.
§4Ключи уникальны
Один ключ хранится в словаре только один раз. Повторное присваивание не добавляет пару, а заменяет значение.
data = {"status": "new", "status": "done"}
print(data)
{'status': 'done'}Ошибки при этом нет — и в этом опасность: опечатка в длинном литерале молча затрёт данные. Значения, в отличие от ключей, повторяться могут сколько угодно: {"x": 1, "y": 1} — совершенно нормальный словарь.
§5Ключи должны быть хешируемыми
Ключом может быть не любой объект. Требование то же, что в главе 4.1: объект должен быть неизменяемым, чтобы у него был постоянный хеш.
data = {[1, 2]: "value"}
TypeError: unhashable type: 'list'
| Годится в ключи | Не годится |
|---|---|
str, int, float, bool | list |
tuple из неизменяемых | dict |
frozenset | set |
На значения таких ограничений нет: значением может быть что угодно — число, строка, список, функция, другой словарь.
§6Доступ по ключу
Квадратные скобки возвращают значение — или прерывают программу, если ключа нет.
user = {"name": "Max", "role": "admin"}
print(user["name"])
print(user["age"])
Max
Traceback (most recent call last):
File "main.py", line 4, in <module>
print(user["age"])
~~~~^^^^^^^
KeyError: 'age'
Это тот же KeyError, что встречался в format_map() из главы 5.6. Он строгий по замыслу: если ключ обязан быть, лучше узнать об его отсутствии сразу.
§7get(): доступ без ошибки
Когда отсутствие ключа — нормальная ситуация, используют get().
user = {"name": "Max"}
print(user.get("age"))
print(user.get("age", 0))
None 0
| Запись | Ключ есть | Ключа нет |
|---|---|---|
data[key] | значение | KeyError |
data.get(key) | значение | None |
data.get(key, default) | значение | default |
Скобки — когда отсутствие ключа означает ошибку в данных и программа не должна продолжать работу. get() — когда ключ действительно может отсутствовать и для этого случая есть разумный запасной вариант.
§8Добавление и обновление
Отдельного метода для добавления нет: присваивание по ключу и создаёт пару, и обновляет её.
user = {}
user["name"] = "Max"
user["role"] = "student"
user["role"] = "teacher"
print(user)
{'name': 'Max', 'role': 'teacher'}Заметное отличие от списка: numbers[5] = x для короткого списка даст IndexError, а data["new"] = x для словаря просто создаст ключ. Читать несуществующий ключ нельзя, писать в него — можно.
§9Удаление пары
| Запись | Что делает | Возвращает | Если ключа нет |
|---|---|---|---|
del data[key] | удаляет пару | ничего | KeyError |
data.pop(key) | удаляет пару | её значение | KeyError |
data.pop(key, default) | удаляет, если есть | значение или default | возвращает default |
data.popitem() | удаляет последнюю пару | кортеж (key, value) | KeyError на пустом |
data.clear() | удаляет все пары | None | — |
user = {"name": "Max", "role": "admin"}
role = user.pop("role")
print(role)
print(user)
admin
{'name': 'Max'}Логика та же, что у pop() списков из главы 3.11, только выбор идёт по ключу, а не по индексу. Форма с запасным значением избавляет от предварительной проверки.
§10in проверяет ключи
Оператор in для словаря смотрит только на ключи — это регулярно становится неожиданностью.
user = {"name": "Max", "role": "admin"}
print("name" in user)
print("Max" in user)
print("Max" in user.values())
True False True
Причина в скорости: проверка ключа идёт по хешу и мгновенна, а проверка значения требует перебора всех пар. Умолчание выбрано в пользу быстрой операции.
§11Перебор словаря
Цикл по самому словарю выдаёт ключи. Для значений и пар есть отдельные методы.
prices = {"book": 900, "pen": 120}
print(list(prices))
print(list(prices.values()))
print(list(prices.items()))
['book', 'pen']
[900, 120]
[('book', 900), ('pen', 120)]Самая частая форма — перебор пар с распаковкой прямо в заголовке цикла, как в главе 4.1.
for name, price in prices.items(): print(name, price)
book 900 pen 120
Методы keys(), values() и items() подробно разбираются в главах 4.3 и 4.4.
§12Изменение размера в цикле
Здесь словарь строже списка. Если в главе 3.11 удаление элементов во время перебора приводило к тихо неверному результату, то словарь такую попытку прерывает.
prices = {"book": 900, "pen": 120}
for key in prices:
prices["new"] = 1
RuntimeError: dictionary changed size during iteration
Обратите внимание: запрещено менять размер, а не содержимое. Присваивание prices["book"] = 950 существующему ключу во время перебора допустимо. Если добавлять или удалять пары всё-таки нужно, перебирают копию ключей: for key in list(prices).
§13Практика: счётчик
Самая частая задача на словари — посчитать, сколько раз что-то встречается. Здесь get() с запасным значением незаменим.
text = "кот пёс кот" counts = {} for word in text.split(): counts[word] = counts.get(word, 0) + 1 print(counts)
{'кот': 2, 'пёс': 1}- Словарь создаётся пустым — до цикла, как список в главе 3.10.
counts.get(word, 0)даёт текущее число или0, если слово встретилось впервые.- К нему прибавляется единица, и результат записывается обратно по тому же ключу.
Без get() пришлось бы писать проверку if word in counts с двумя ветками. Значение по умолчанию сводит их к одной строке.
§14Вложенные словари
Значением может быть другой словарь — так описывают наборы однотипных записей.
users = {
101: {"name": "Max", "active": True},
102: {"name": "Anna", "active": False},
}
print(users[101]["name"])
Max
Двойные скобки читаются так же, как у вложенных списков из главы 3.1: сначала users[101] даёт внутренний словарь, затем из него берётся "name". Так устроен и JSON — формат обмена данными, который разбирается в модуле 8.
§15Создание из пар
dict() собирает словарь из готовых пар — например, из списка кортежей или из двух параллельных списков через zip() из главы 3.14.
pairs = [("a", 1), ("b", 2)] keys = ["x", "y"] values = [10, 20] print(dict(pairs)) print(dict(zip(keys, values)))
{'a': 1, 'b': 2}
{'x': 10, 'y': 20}Обратное преобразование даёт list(data.items()). Есть и третий способ — словарное включение, ему посвящена глава 4.10.
§16Почему доступ быстрый
Чтобы найти значение в списке, Python сравнивает элементы по очереди: в списке из миллиона записей это до миллиона сравнений. Словарь устроен иначе.
- Python вычисляет
hash(key)— то самое число из главы 4.1. - По хешу сразу определяет область внутренней таблицы, где должно лежать значение.
- Сверяет ключ и возвращает связанное с ним значение.
Поиск не зависит от размера словаря: и в десяти парах, и в миллионе он занимает примерно одинаковое время. Это записывают как O(1) — сложность из курса алгоритмов. Гарантия средняя, а не абсолютная, но на практике это именно так.
Если по данным приходится часто искать — переложите их из списка в словарь, где ключом станет то, по чему ищете. Один проход на построение словаря окупается на первых же десятках поисков.
products = [
{"sku": "A-10", "price": 900},
{"sku": "B-20", "price": 120},
]
by_sku = {product["sku"]: product for product in products}
print(by_sku["B-20"]["price"])
120
§17Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
data["missing"] | KeyError, если ключа нет | data.get("missing") или проверка in |
{[1, 2]: "value"} | TypeError: unhashable type: 'list' | {(1, 2): "value"} |
"Max" in user для поиска значения | проверится ключ, результат False | "Max" in user.values() |
| повторить ключ в литерале | останется последнее значение, без ошибки | следить за уникальностью ключей |
добавлять пары внутри for key in data | RuntimeError | for key in list(data) |
data[0] для первого элемента | ищется ключ 0, обычно KeyError | обращаться по ключу или взять list(data)[0] |
counts[word] += 1 для нового слова | KeyError при первом появлении | counts[word] = counts.get(word, 0) + 1 |
data.pop(key) без запасного значения | KeyError, если ключа нет | data.pop(key, None) |
§18Проверьте себя
Чем отличаются data[key] и data.get(key)?
Скобки вызывают KeyError при отсутствии ключа, get() возвращает None или переданное значение по умолчанию.
Что проверяет key in data?
Наличие ключа. Для поиска среди значений нужно явно взять data.values() — эта проверка идёт перебором и потому медленнее.
Почему список нельзя использовать как ключ?
Список изменяем, а значит у него нет постоянного хеша, по которому словарь находит значение.
Что произойдёт, если записать один ключ дважды?
Останется последнее значение. Ошибки не будет — это и делает опечатку в ключе опасной.
Почему data["new"] = 1 работает, а data["new"] — нет?
Запись по новому ключу создаёт пару, а чтение требует, чтобы ключ уже существовал.
Как посчитать количество повторов, не проверяя наличие ключа?
counts[word] = counts.get(word, 0) + 1: запасной ноль заменяет ветку «ключа ещё нет».
§19Шпаргалка
data = {"id": 101}data["id"]data.get("role", "guest")data["role"] = "admin"data.pop("role", None)"id" in datafor k, v in data.items():c[x] = c.get(x, 0) + 1Итог главы
Словарь хранит пары с уникальными хешируемыми ключами и находит значение по ключу за постоянное время, не зависящее от размера. Квадратные скобки дают строгий доступ с KeyError, get() — доступ с запасным результатом, а присваивание по ключу одновременно добавляет и обновляет пару. Оператор in и цикл по словарю работают с ключами, а не со значениями, и менять размер словаря во время перебора нельзя.