Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 9 из 20 Начальный 20 мин 120 XP

Вложенные словари: словарь внутри словаря

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

Редакция Питоники

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

Словарь как значение: каталог товаров

каталог: товар — его паспорт
goods = {
    'молоко': {'цена': 89, 'остаток': 12},
    'хлеб': {'цена': 42, 'остаток': 5},
    'сыр': {'цена': 590, 'остаток': 3},
}

print(goods['молоко'])
print(goods['молоко']['цена'])
Вывод
{'цена': 89, 'остаток': 12}
89

Внешний ключ выбирает товар — и возвращает его словарь-паспорт. Внутренний ключ выбирает поле паспорта. Каждая ступень цепочки — отдельный вопрос: внешний ключ отвечает, какой это товар, внутренний — что именно про него знаем. Читать запись удобно слева направо: сначала «молоко», затем «цена».

Запись по цепочке и страховка get

меняем вложенное поле
goods = {
    'молоко': {'цена': 89, 'остаток': 12},
    'сыр': {'цена': 590, 'остаток': 3},
}

goods['сыр']['остаток'] = 1
print(goods['сыр'])

print(goods.get('кефир', {}).get('цена'))
Вывод
{'цена': 590, 'остаток': 1}
None

Присваивание по цепочке меняет ровно одно поле: сыр остался сыром, остаток стал единицей. Опасно другое: KeyError прилетит при отсутствии любого ключа ступени — и товара, и поля; урок 3 показывал, как это выглядит. Страховка — get с пустым словарём по умолчанию: если кефира нет, внешний get вернёт {}, и внутренний get молча ответит None.

Перебор вложенного словаря

один цикл — один товар за шаг
goods = {
    'молоко': {'цена': 89, 'остаток': 12},
    'хлеб': {'цена': 42, 'остаток': 5},
}

for name, info in goods.items():
    print(name, '-', info['цена'], 'руб.,', info['остаток'], 'шт.')
Вывод
молоко - 89 руб., 12 шт.
хлеб - 42 руб., 5 шт.

Распаковка из урока 8 работает и здесь: в info приходит словарь конкретного товара, из которого поля достаются по именам. Для двух уровней вложенные циклы не нужны — одного обхода хватает. Глубже двух уровней структуры встречаются заметно реже и читаются тяжелее.

Где ломается цепочка

Ключевая особенность вложенного словаря: цепочка доступа падает не на одном месте, а на любой ступени. Строка goods['кефир']['цена'] читается как «взять по ключу 'кефир', потом по ключу 'цена'». Если кефира нет — падает первое обращение, и до 'цена' дело не доходит вообще. Проверим.

два уровня страховки
goods = {'сыр': {'цена': 590, 'остаток': 3}}

try:
    print(goods['кефир']['цена'])
except KeyError as e:
    print('KeyError:', e)

print(goods.get('кефир', {}).get('цена'))
print(goods.get('сыр', {}).get('цена'))
Вывод
KeyError: 'кефир'
None
590

Первая строка — упавший строгий доступ, второй — мягкий вариант. Разберём, почему он работает: внешний get с умолчанием {} отдаёт пустой словарь, если кефира нет; у пустого словаря нет ключа 'цена', и внутренний get без умолчания честно отвечает None. Цепочка из двух get гасит обе ступени.

страховка одним get
goods = {'сыр': {'цена': 590}}

print(goods.get('кефир').get('цена'))
Вывод
Traceback (most recent call last):
  File "example.py", line 1, in <module>
    print(goods.get('кефир').get('цена'))
AttributeError: 'NoneType' object has no attribute 'get'
Блок не запускается: код специально падает. Ошибка AttributeError, а не KeyError, — потому что внешний get вернул None, а не словарь.

Сценарий: настройки приложения

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

настройки с вложенной группой
settings = {'theme': 'dark', 'lang': 'ru'}
settings['limits'] = {'per_day': 100}

print(settings)
print(settings['limits']['per_day'])
Вывод
{'theme': 'dark', 'lang': 'ru', 'limits': {'per_day': 100}}
100

Тут есть тонкость, на которой спотыкаются: когда вложенный словарь создаётся присваиванием в пустое место, он не связан с остальным словарём никак, кроме того, что теперь лежит под ключом 'limits'. Но стоит написать settings['limits']['per_day'] = 200 — и изменение уйдёт в тот же вложенный словарь, потому что settings['limits'] вернул именно его. Ссылка тут одна и та же, а не копия.

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

Считаем по вложенному словарю

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

сумма по вложенным полям
goods = {
    'молоко': {'цена': 89, 'остаток': 12},
    'сыр': {'цена': 590, 'остаток': 3},
}

total = 0
for name, info in goods.items():
    total += info['цена'] * info['остаток']

print(total)
Вывод
2838

В цикле name, info — это имя товара и его словарь-паспорт. Дальше идёт обычная арифметика по полям паспорта: цена умножается на остаток. Итог 2838 — это 89 × 12 плюс 590 × 3. Никакой вложенный цикл не нужен: двух уровней хватает, чтобы пройтись по товарам и собрать сумму.

а если полей больше двух
goods = {'молоко': {'цена': 89, 'остаток': 12}}

for name, info in goods.items():
    for field, value in info.items():
        print(name, field, value)
Вывод
молоко цена 89
молоко остаток 12

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

Словарь словарей или список словарей

Два уровня — не единственный способ хранить каталог. Товары можно положить и списком словарей: каждый товар — словарь, а рядом стоят имя и порядок. Разница в главном: словарь даёт доступ по имени за один шаг, список — только перебором. Если к товару обращаются по названию, словарь удобнее; если важен сам порядок, — список.

один и тот же каталог двумя способами
as_dict = {'молоко': {'цена': 89}, 'сыр': {'цена': 590}}

as_list = [
    {'name': 'молоко', 'цена': 89},
    {'name': 'сыр', 'цена': 590},
]

print(as_dict['молоко']['цена'])
print(as_list[0]['цена'])
print(len(as_dict), len(as_list))
Вывод
89
89
2 2

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

Копия вложенного словаря

Отдельная ловушка — копирование. dict(goods) и goods.copy() копируют только верхний уровень: вложенные словари-паспорта остаются общими с оригиналом. Правка паспорта в копии тут же видна в оригинале, хотя на верхнем уровне всё выглядит независимым.

копия делит паспорта с оригиналом
goods = {'молоко': {'цена': 89, 'остаток': 12}}

draft = goods.copy()
draft['молоко']['остаток'] = 0

print(goods)
print(draft)
Вывод
{'молоко': {'цена': 89, 'остаток': 0}}
{'молоко': {'цена': 89, 'остаток': 0}}

Оба словаря показывают остаток 0 — хотя меняли мы только копию. Если бы таких копий было несколько, правка одного паспорта разъехалась бы по всем. Для настоящей независимости нужен copy.deepcopy — про все виды копирования словарей подробно в уроке 19.

Обновление поля без потери остальных

И последняя ловушка, на которой чаще всего спотыкаются при работе с паспортами, — перезапись словаря целиком, чтобы поменять одно поле. Строка goods['сыр'] = {'цена': 700} уничтожит остаток, который никто не просил менять. Правильная запись — по цепочке: goods['сыр']['цена'] = 700.

меняем одно поле, не теряя остальные
goods = {'сыр': {'цена': 590, 'остаток': 3}}

goods['сыр'] = {'цена': 700}
print(goods)

goods = {'сыр': {'цена': 590, 'остаток': 3}}
goods['сыр']['цена'] = 700
print(goods)
Вывод
{'сыр': {'цена': 700}}
{'сыр': {'цена': 700, 'остаток': 3}}

Что дальше

Вложенные словари разобраны: цепочка доступа, страховка через get, перебор и подсчёт. Дальше — группировка, когда под каждым ключом должен лежать список. В уроке 10 разбираем setdefault, который создаёт список при первом обращении, а в уроке 12 — defaultdict, который делает то же самое, но удобнее.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

goods = {'хлеб': {'цена': 42, 'остаток': 5}}

print(goods['хлеб']['остаток'])
cat = {'кот': {'имя': 'Барсик', 'возраст': 3}}

cat['кот']['возраст'] += 1
print(cat['кот']['возраст'])
goods = {'сыр': {'цена': 590}}

print(goods.get('молоко', {}).get('цена'))
print(len(goods['сыр']))
Проверь себя
0 / 8

1. Что вернёт goods['молоко'], если под этим ключом лежит {'цена': 89, 'остаток': 12}?

2. Как достать цену хлеба из каталога goods?

3. Товара 'кефир' в словаре нет. Что произойдёт при goods['кефир']['цена']?

4. Что распаковывает заголовок for name, info in goods.items():?

5. Что вернёт goods.get('кефир', {}).get('цена'), если кефира нет?

6. Что произойдёт при goods.get('кефир').get('цена'), если кефира нет?

7. Сколько уровней вложенности разумно держать в словаре?

8. Что вернёт len(goods) для каталога из трёх товаров, у каждого по два поля?

Карточки терминов
Запомнено: 0 / 9
Практика

Собери каталог goods с двумя товарами: молоко (цена 89, остаток 12) и сыр (цена 590, остаток 3). Выведи цену молока по цепочке ключей, затем стоимость всего молока — цену, умноженную на остаток.

practice.py
Вопросы и ответы по уроку

Что такое вложенный словарь в Python?

Это словарь, значением которого служит другой словарь: {'молоко': {'цена': 89, 'остаток': 12}}. Внешний ключ выбирает сущность, внутренние ключи — её свойства. Такие структуры типичны для каталогов, настроек и JSON.

Урок 8: перебор через items

Как обратиться к значению во вложенном словаре?

По цепочке ключей: goods['молоко']['цена'] — сначала ключ товара во внешнем словаре, затем ключ поля во внутреннем. Присваивание по той же цепочке меняет вложенное значение.

Как избежать KeyError во вложенном словаре?

Строгие скобки падают на любой отсутствующей ступени. Страховка — get с пустым словарём по умолчанию: goods.get('кефир', {}).get('цена') вернёт None вместо падения. А когда отсутствие ключа означает баг, пусть падает громко.

Урок 3: KeyError

Где используются вложенные словари?

В JSON-ответах API, конфигурациях и каталогах: два уровня покрывают почти всё — ключ на сущность, словарь на её свойства. Глубже двух уровней цепочки читаются плохо; там лучше список словарей или отдельный класс.

Раздел про json

Почему goods.get('кефир').get('цена') упадёт?

Внешний get без умолчания вернёт None, потому что ключа 'кефира' нет. У None нет метода get, и Python выбросит AttributeError. Правильный вариант — goods.get('кефир', {}).get('цена'): пустой словарь по умолчанию гасит первую ступень, и цепочка доходит до конца, возвращая None.

Как посчитать сумму по вложенным полям?

Одним циклом по внешнему словарю: for name, info in goods.items(): и дальше total += info['цена'] * info['остаток']. Вложенный цикл не нужен — двух уровней хватает, чтобы пройтись по товарам и собрать сумму. Если полей становится много и появляется второй цикл, стоит задуматься о смене структуры.

Урок 8: перебор через items

Почему goods.copy() не делает полную копию?

Копия создаётся только на верхнем уровне: вложенные словари-паспорта остаются общими с оригиналом, поэтому правка goods['сыр']['цена'] в копии тут же появится и в оригинале. Для настоящей независимости нужен copy.deepcopy — он копирует вложенные структуры целиком.

Урок 19: копирование словарей

Понравился урок? Сошлитесь на него

«Каждая ступень цепочки — отдельный вопрос: внешний ключ отвечает, какой это товар, внутренний — что именно про него знаем.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.