Глава 5. Словари: карточка ученика и группировка по предметам
Задача главы: научиться хранить разнородные данные одного ученика в словаре по именованным полям, безопасно доставать значения, обходить словарь и накапливать в нём группировки — а также разбираться во вложенных структурах, которые моделируют настоящую выгрузку из журнала.
5.1 Карточка ученика: dict, доступ по ключу, добавление и изменение полей
Зачем это нужно. У списка из главы Списки есть позиция, но нет имени поля — «третий элемент» ничего не говорит о смысле. Карточке ученика нужны названные поля: имя, группа, баллы по предметам. Для этого в Python есть словарь — коллекция пар «ключ → значение».
Одна и та же операция присваивания (dict[key] = value) добавляет новое поле, если такого ключа не было, и заменяет значение, если ключ уже существовал, — Python сам решает, что нужно, по факту наличия ключа.
⚠️ Подводный камень
Обращение по ключу, которого нет в словаре, не возвращает «пусто» или None — оно ломает программу.
Traceback (most recent call last): File "report.py", line 7, in <module> literature_score = student['literature']KeyError: 'literature'| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
KeyError | «Ключа 'literature' в словаре нет» | В карточке ученика нет этого поля — предмет не сдавался, либо в названии ключа опечатка. Прямое обращение dict[key] требует гарантии, что ключ существует. |
Правильный паттерн
Пояснение логики: .get(key, default) устроен как вопрос «а если такого ключа нет?» — вместо падения он тихо возвращает значение по умолчанию. Для обязательных полей (например, 'name' — карточка без имени бессмысленна) прямое обращение dict[key] даже полезно: KeyError сразу укажет на кривую запись. А вот для необязательных полей вроде отдельных предметов .get() — стандартный безопасный выбор.
5.2 Обход словаря: .keys(), .values(), .items()
Зачем это нужно. Часто нужно не значение одного конкретного поля, а вся карточка целиком — вывести её построчно в отчёт или просуммировать все баллы, не перечисляя предметы по имени вручную.
⚠️ Подводный камень
Обход for x in dict по умолчанию перебирает ключи, а не значения. На отдельной карточке ученика это не бросается в глаза, но на словаре «предмет → балл» ломает расчёт.
Traceback (most recent call last): File "report.py", line 5, in <module> total_score += gradeTypeError: unsupported operand type(s) for +=: 'int' and 'str'| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
TypeError | «Нельзя сложить int и str» | for grade in subject_scores перебирает названия предметов (ключи), а не баллы (значения). Переменная grade на самом деле хранила строку 'math'. |
Правильный паттерн
Пояснение логики: for x in dict — это сокращение для for x in dict.keys(), поведение задано языком, а не ошибка интерпретатора. Если нужны значения — .values(), если нужны и ключ, и значение вместе — .items(). Явное имя метода избавляет от догадок о том, что именно лежит в переменной цикла.
5.3 Группировка по предметам: накопление в словаре, setdefault()
Зачем это нужно. Реальная выгрузка из журнала — это плоский список записей «ученик, предмет, балл», а не готовая карточка. Чтобы посчитать средний балл по каждому предмету, записи нужно сгруппировать — накопить баллы по ключу-предмету.
setdefault(key, default) — это одна операция вместо «проверить, есть ли ключ, и если нет — создать его»: если ключа не было, он появляется со значением default и тут же возвращается; если ключ уже был, возвращается его текущее значение, ничего не перезаписывая.
💡 На заметку: В профессиональной аналитике для автосоздания списков также часто используют
defaultdict(list)из встроенного модуляcollections— его мы мельком коснёмся в Главе 12.
⚠️ Подводный камень
Словарь нельзя менять по составу ключей прямо во время обхода for key in dict — ни добавлять, ни удалять.
Traceback (most recent call last): File "report.py", line 4, in <module> for subject in scores_by_subject:RuntimeError: dictionary changed size during iteration| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
RuntimeError | «Размер словаря изменился во время обхода» | Внутри for subject in scores_by_subject удалили ключ из этого же словаря. Python отслеживает состав ключей во время цикла и не разрешает его менять «под ногами». |
Правильный паттерн
Пояснение логики: list(scores_by_subject) создаёт отдельный список ключей — точно так же, как [:] в главе Списки создавал независимую копию списка. Цикл теперь идёт по этой копии, а сам словарь можно спокойно менять внутри тела цикла: копия уже не следит за его текущим составом.
5.4 Вложенные структуры: словарь словарей и список словарей как модель выгрузки
Зачем это нужно. Настоящая выгрузка — это обычно много учеников, и у каждого несколько полей. Такую структуру моделируют либо списком словарей (одна запись — один словарь, порядок как в файле), либо словарём словарей (быстрый доступ по ФИО без перебора).
⚠️ Подводный камень
Чем глубже вложенность, тем легче потерять счёт уровням — обратиться на один уровень глубже, чем есть на самом деле.
Traceback (most recent call last): File "report.py", line 4, in <module> math_score = student['name']['math']TypeError: string indices must be integers| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
TypeError | «У строки нет индекса 'math', строки индексируются числами» | student['name'] — это уже строка 'Иванов Иван', а не вложенный словарь. Обращение ['math'] к ней означает попытку взять срез строки по текстовому ключу. Сигнал: структура на самом деле на один уровень площе, чем предполагалось в коде. |
Правильный паттерн
Пояснение логики: перед тем как писать data[key1][key2], стоит явно проверить (например, через print(type(data[key1]))), что data[key1] — действительно ещё один словарь, а не строка или число. Вложенность оправдана, только когда за первым ключом реально стоит вторая коллекция.
5.5 Мини-словарик ошибок пятой главы
| Ошибка | Что значит для Python | Человеческий перевод |
|---|---|---|
KeyError | «Такого ключа нет в словаре» | Обратились к полю, которого нет в карточке, — опечатка в названии или предмет не сдавали. Для необязательных полей используйте .get(key, default). |
TypeError (сложение) | «Нельзя сложить int и str» | Обход for x in dict даёт ключи, а не значения — для сумм и средних нужен .values(). |
TypeError (вложенность) | «У строки нет индекса-строки» | Перепутан уровень вложенности: обратились как к словарю словарей туда, где на самом деле одна плоская карточка. |
RuntimeError | «Размер словаря изменился во время обхода» | Добавили или удалили ключ прямо в цикле for key in dict. Сначала соберите снимок ключей list(dict), потом меняйте словарь. |
| — (тихая ошибка) | «Разным ключам присвоен один и тот же изменяемый объект по умолчанию» | Список-накопитель, переданный вторым аргументом в несколько вызовов setdefault(), — это один и тот же объект в памяти: пополнение через один ключ видно и у другого. |
Последняя строка — самая незаметная, потому что traceback здесь не появляется вовсе, программа просто считает неверно:
Причина та же, что и с backup = original в главе Списки: empty_list — не «шаблон нового списка», а конкретный объект, и оба ключа получили ссылку на один и тот же список. Исправление — создавать новый список прямо в вызове: scores_by_subject.setdefault(subject, []), без переменной-прослойки. ⚠️ Похожая ловушка с изменяемым значением по умолчанию поджидает в главе Функции — там она прячется в аргументах функции.
Мини-кейс: свод по предметам — средний балл и доля сдавших по каждому
Задача: по плоским записям журнала (ученик, предмет, балл) сгруппировать баллы по предметам и для каждого предмета посчитать средний балл и долю сдавших порог.
--- Свод по предметам ---math: средний балл 71.7, сдали 67%physics: средний балл 79.3, сдали 67%Что получилось: setdefault(subject, []) избавил от отдельной проверки «уже встречался этот предмет или нет» при сборе scores_by_subject. Результат — словарь словарей: внешний ключ — предмет, внутренний словарь — пара метрик average и pass_rate, из которого готовая строка отчёта достаётся по имени поля, а не по позиции. ⚠️ Если бы в записи журнала отсутствовал ключ 'score' (кривая строка выгрузки), record['score'] упал бы с KeyError ещё на шаге расчётов — устойчивый разбор такой выгрузки ждёт в главе Ошибки и исключения.
Чек-лист главы
- Храню разнородные поля одного ученика в словаре с именованными ключами, а не в списке по позициям
- Добавляю и меняю поля словаря одной и той же операцией
dict[key] = value - Помню, что обращение
dict[key]к отсутствующему ключу даётKeyError - Достаю необязательное поле через
.get(key, default), а не через прямое обращение по ключу - Обхожу словарь через
.items(), когда нужны и ключ, и значение одновременно - Помню, что
for x in dictперебирает ключи, а не значения, — для значений нужен.values() - Накопливаю группировку в словаре через
setdefault(key, []), не проверяя вручную «есть ли уже такой ключ» - Не меняю состав ключей словаря прямо во время обхода
for key in dict— сначала беру снимокlist(dict) - Различаю словарь словарей (
dict[name][field]) и список словарей (list[i][field]) и не путаю уровни вложенности - Узнаю
TypeErrorпро индексы строки как сигнал «обратился на уровень глубже, чем есть на самом деле» - Не передаю один и тот же изменяемый объект вторым аргументом в несколько вызовов
setdefault()
Дальше: Множества и кортежи