Модуль 4 · Словари, множества и кортежи

Справочник по Python · модуль 4 · словари, множества и кортежи

Глава 4.2

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) значение находят по уникальному ключу.

"id"→101
"name"→"Max"
"role"→"admin"

Начиная с Python 3.7 словарь гарантированно сохраняет порядок добавления пар. Но это лишь порядок вывода: обращаться к словарю по числовым позициям всё равно нельзя — data[0] означает поиск ключа 0, а не первого элемента.

§3Создание словаря

main.pyлитерал, функция dict и пустой словарь
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Ключи уникальны

Один ключ хранится в словаре только один раз. Повторное присваивание не добавляет пару, а заменяет значение.

main.pyключ повторён в литерале
data = {"status": "new", "status": "done"}
print(data)
Выводосталось последнее значение
{'status': 'done'}

Ошибки при этом нет — и в этом опасность: опечатка в длинном литерале молча затрёт данные. Значения, в отличие от ключей, повторяться могут сколько угодно: {"x": 1, "y": 1} — совершенно нормальный словарь.

§5Ключи должны быть хешируемыми

Ключом может быть не любой объект. Требование то же, что в главе 4.1: объект должен быть неизменяемым, чтобы у него был постоянный хеш.

main.pyсписок в роли ключа
data = {[1, 2]: "value"}
Ошибказнакомое сообщение
TypeError: unhashable type: 'list'
Годится в ключиНе годится
str, int, float, boollist
tuple из неизменяемыхdict
frozensetset

На значения таких ограничений нет: значением может быть что угодно — число, строка, список, функция, другой словарь.

§6Доступ по ключу

Квадратные скобки возвращают значение — или прерывают программу, если ключа нет.

main.pyключа age в словаре нет
user = {"name": "Max", "role": "admin"}

print(user["name"])
print(user["age"])
Вывод и ошибкаKeyError называет ключ
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().

main.pyбез запасного значения и с ним
user = {"name": "Max"}

print(user.get("age"))
print(user.get("age", 0))
ВыводNone и значение по умолчанию
None
0
ЗаписьКлюч естьКлюча нет
data[key]значениеKeyError
data.get(key)значениеNone
data.get(key, default)значениеdefault
Как выбрать

Скобки — когда отсутствие ключа означает ошибку в данных и программа не должна продолжать работу. get() — когда ключ действительно может отсутствовать и для этого случая есть разумный запасной вариант.

§8Добавление и обновление

Отдельного метода для добавления нет: присваивание по ключу и создаёт пару, и обновляет её.

main.pyключа ещё нет, затем он уже есть
user = {}

user["name"] = "Max"
user["role"] = "student"
user["role"] = "teacher"

print(user)
Выводдве пары, role обновлён
{'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—
main.pypop() возвращает удалённое значение
user = {"name": "Max", "role": "admin"}
role = user.pop("role")

print(role)
print(user)
Выводзначение получено, пара удалена
admin
{'name': 'Max'}

Логика та же, что у pop() списков из главы 3.11, только выбор идёт по ключу, а не по индексу. Форма с запасным значением избавляет от предварительной проверки.

§10in проверяет ключи

Оператор in для словаря смотрит только на ключи — это регулярно становится неожиданностью.

main.pyтри проверки
user = {"name": "Max", "role": "admin"}

print("name" in user)
print("Max" in user)
print("Max" in user.values())
Выводпо значению нужно спрашивать явно
True
False
True

Причина в скорости: проверка ключа идёт по хешу и мгновенна, а проверка значения требует перебора всех пар. Умолчание выбрано в пользу быстрой операции.

§11Перебор словаря

Цикл по самому словарю выдаёт ключи. Для значений и пар есть отдельные методы.

main.pyтри способа перебора
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.

main.pyключ и значение сразу
for name, price in prices.items():
    print(name, price)
Выводдве пары
book 900
pen 120

Методы keys(), values() и items() подробно разбираются в главах 4.3 и 4.4.

§12Изменение размера в цикле

Здесь словарь строже списка. Если в главе 3.11 удаление элементов во время перебора приводило к тихо неверному результату, то словарь такую попытку прерывает.

main.pyдобавление ключа во время перебора
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() с запасным значением незаменим.

main.pyподсчёт слов в тексте
text = "кот пёс кот"
counts = {}

for word in text.split():
    counts[word] = counts.get(word, 0) + 1

print(counts)
Выводслово и число повторений
{'кот': 2, 'пёс': 1}
  1. Словарь создаётся пустым — до цикла, как список в главе 3.10.
  2. counts.get(word, 0) даёт текущее число или 0, если слово встретилось впервые.
  3. К нему прибавляется единица, и результат записывается обратно по тому же ключу.

Без get() пришлось бы писать проверку if word in counts с двумя ветками. Значение по умолчанию сводит их к одной строке.

§14Вложенные словари

Значением может быть другой словарь — так описывают наборы однотипных записей.

main.pyпользователи по идентификатору
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.

main.pyдва источника пар
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 сравнивает элементы по очереди: в списке из миллиона записей это до миллиона сравнений. Словарь устроен иначе.

  1. Python вычисляет hash(key) — то самое число из главы 4.1.
  2. По хешу сразу определяет область внутренней таблицы, где должно лежать значение.
  3. Сверяет ключ и возвращает связанное с ним значение.

Поиск не зависит от размера словаря: и в десяти парах, и в миллионе он занимает примерно одинаковое время. Это записывают как O(1) — сложность из курса алгоритмов. Гарантия средняя, а не абсолютная, но на практике это именно так.

Отсюда вывод для практики

Если по данным приходится часто искать — переложите их из списка в словарь, где ключом станет то, по чему ищете. Один проход на построение словаря окупается на первых же десятках поисков.

main.pyиндекс товаров по артикулу
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 dataRuntimeErrorfor 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 data
Перебрать парыfor k, v in data.items():
Счётчикc[x] = c.get(x, 0) + 1

Итог главы

Словарь хранит пары с уникальными хешируемыми ключами и находит значение по ключу за постоянное время, не зависящее от размера. Квадратные скобки дают строгий доступ с KeyError, get() — доступ с запасным результатом, а присваивание по ключу одновременно добавляет и обновляет пару. Оператор in и цикл по словарю работают с ключами, а не со значениями, и менять размер словаря во время перебора нельзя.

Глава 4.2 · Словари · Справочник по Python Автор: Макаров Максим Николаевич