Словари в Python на практике: от создания до копирования
Работа со словарями в Python сводится к нескольким операциям: создать словарь, получить значение по ключу, добавить или удалить пару, перебрать содержимое. В статье каждая операция показана на коротком примере с результатом, рядом описаны частые ошибки: KeyError, изменение словаря во время цикла, поверхностная копия. В конце есть сводная таблица методов и ответы на типичные вопросы.

Содержание статьи
Как устроен словарь и как его создать
Словарь (тип dict) хранит пары ключ-значение и находит значение по ключу за постоянное время, независимо от размера. Ключом может быть только хешируемый объект: строка, число, кортеж из неизменяемых элементов, frozenset. Список или другой словарь ключом быть не могут, Python выбросит TypeError: unhashable type. Значения могут быть любыми, в том числе списками и вложенными словарями.
Начиная с Python 3.7 словарь гарантированно помнит порядок добавления ключей. Поэтому утверждение "словарь неупорядочен" из старых учебников уже неверно: при выводе и переборе пары идут в том порядке, в котором их добавили.
empty = {}
user = {"name": "Тимур", "age": 30}
same = dict(name="Тимур", age=30) # ключи только строки
pairs = dict([("a", 1), ("b", 2)]) # из списка пар
zipped = dict(zip(["x", "y"], [10, 20])) # из двух списков
marks = dict.fromkeys(["math", "physics"], 0)
print(zipped) # {'x': 10, 'y': 20}
print(marks) # {'math': 0, 'physics': 0}Не называйте переменную dict: имя встроенного типа будет перекрыто, и вызов dict(...) ниже по коду перестанет работать. Повторяющийся ключ в литерале не вызывает ошибку, просто последнее значение затирает предыдущее.
Получение значения по ключу
Обращение через квадратные скобки возвращает значение или выбрасывает KeyError, если ключа нет. Метод get() в этой ситуации возвращает None или значение по умолчанию, переданное вторым аргументом. Проверить наличие ключа можно оператором in, он смотрит именно на ключи, а не на значения.
user = {"name": "Тимур", "age": 30}
print(user["name"]) # Тимур
print(user.get("city")) # None
print(user.get("city", "нет")) # нет
print("age" in user) # True
print(30 in user) # False, 30 это значение
print(30 in user.values()) # TrueСкобки уместны, когда ключ обязан быть: отсутствие ключа тогда означает ошибку в данных, и лучше узнать о ней сразу. Метод get() подходит для необязательных полей, например при разборе JSON-ответа сервера.
Добавление, изменение и объединение
Присваивание d[key] = value создает новую пару или перезаписывает значение существующего ключа. Чтобы перенести сразу несколько пар, используют метод update() или операторы | и |=, которые появились в Python 3.9.
user = {"name": "Тимур", "age": 30}
user["age"] = 31 # изменение
user["city"] = "Москва" # добавление
extra = {"city": "Казань", "email": "t@example.com"}
merged = user | extra # новый словарь, user не меняется
user.update(extra) # меняет user на месте
print(merged["city"]) # Казань: при совпадении побеждает правыйЗначение по умолчанию через setdefault()
Метод setdefault(key, default) возвращает значение ключа, а если ключа нет, сначала записывает в словарь default. Это удобно при группировке, когда для каждого ключа копится список.
words = ["кот", "кит", "дом", "дым", "сад"]
groups = {}
for w in words:
groups.setdefault(w[0], []).append(w)
print(groups) # {'к': ['кот', 'кит'], 'д': ['дом', 'дым'], 'с': ['сад']}Ту же задачу решает collections.defaultdict(list): он создает пустой список при первом обращении к новому ключу, и код становится короче.
Удаление элементов
Для удаления есть четыре способа, они отличаются тем, что возвращают и как ведут себя при отсутствии ключа.
del d[key]удаляет пару и ничего не возвращает, при отсутствии ключа будетKeyError.d.pop(key, default)удаляет пару и возвращает значение. Если ключа нет иdefaultне передан, тожеKeyError.d.popitem()удаляет и возвращает последнюю добавленную пару в виде кортежа. На пустом словаре выбрасываетKeyError.d.clear()очищает словарь целиком. Скобки обязательны: записьd.clearбез вызова ничего не делает.
squares = {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(squares.pop(4)) # 16
print(squares.pop(10, None)) # None, без ошибки
print(squares.popitem()) # (5, 25)
del squares[1]
print(squares) # {2: 4, 3: 9}
squares.clear()
print(squares) # {}Перебор словаря
Цикл for по словарю перебирает ключи. Для значений есть метод values(), для пар сразу с распаковкой метод items(). Все три метода возвращают представления (view): это не копии, а окна в живой словарь, они отражают последующие изменения.
prices = {"хлеб": 50, "молоко": 90, "сыр": 400}
for name, price in prices.items():
print(f"{name}: {price} руб.")
total = sum(prices.values())
expensive = [k for k, v in prices.items() if v > 80]
print(total, expensive) # 540 ['молоко', 'сыр']Добавлять или удалять ключи прямо во время перебора нельзя, Python остановит цикл с ошибкой RuntimeError: dictionary changed size during iteration. Перебирайте копию списка ключей for k in list(d): или соберите новый словарь генератором. Для вывода по возрастанию значений пригодится сортировка словаря через sorted() с параметром key.
Генераторы словарей и вложенные структуры
Генератор словаря записывается в фигурных скобках через двоеточие и позволяет построить словарь из любой последовательности одной строкой, с фильтром if в конце.
squares = {x: x * x for x in range(1, 6)}
odd = {x: x * x for x in range(10) if x % 2}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(odd) # {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
print(inverted) # {1: 'a', 2: 'b'}Копирование: copy() и deepcopy()
Присваивание b = a не копирует словарь, обе переменные указывают на один объект. Метод copy() создает поверхностную копию: сам словарь новый, но вложенные списки и словари общие. Для полной независимости нужен copy.deepcopy().
import copy
config = {"db": {"host": "localhost"}, "debug": False}
shallow = config.copy()
deep = copy.deepcopy(config)
shallow["db"]["host"] = "10.0.0.1"
print(config["db"]["host"]) # 10.0.0.1, вложенный словарь общий
print(deep["db"]["host"]) # localhostСводная таблица методов dict
Полное описание каждого метода с примерами есть на странице про методы словаря и в официальной документации.
| Метод | Что делает | Возвращает |
|---|---|---|
get(k, d=None) | Читает значение без ошибки | Значение или d |
setdefault(k, d=None) | Читает, при отсутствии записывает d | Значение |
update(other) | Добавляет и перезаписывает пары | None |
pop(k[, d]) | Удаляет пару по ключу | Значение |
popitem() | Удаляет последнюю добавленную пару | Кортеж (k, v) |
keys(), values(), items() | Дают представления ключей, значений, пар | Объект view |
copy() | Поверхностная копия | Новый dict |
fromkeys(seq, v=None) | Создает словарь из ключей с одним значением | Новый dict |
clear() | Удаляет все пары | None |
Встроенные функции тоже работают со словарем: len(d) вернет число пар, sorted(d) отсортированный список ключей, min() и max() сравнивают ключи. Закрепить материал помогут задачи по словарям с решениями.
Частые вопросы
Здесь собраны вопросы, которые возникают у начинающих при работе с dict.
Можно ли использовать список в качестве ключа?
Нет, список изменяемый и не имеет хеша. Преобразуйте его в кортеж: d[tuple(my_list)] = value. Кортеж подойдет, только если внутри него тоже нет списков.
Как найти ключ по значению?
Прямого метода нет, нужен перебор: next((k for k, v in d.items() if v == target), None). Если поиск по значению нужен часто, держите второй, перевернутый словарь.
Как получить ключ с максимальным значением?
Передайте в max() метод get как функцию сравнения: max(d, key=d.get). Для минимального значения так же работает min().
Чем d.keys() отличается от list(d)?
d.keys() возвращает живое представление, которое меняется вместе со словарем и поддерживает операции множеств, например d1.keys() & d2.keys(). list(d) создает независимый список ключей на текущий момент.