Skip to content

Глава 5. Словари: карточка ученика и группировка по предметам

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


5.1 Карточка ученика: dict, доступ по ключу, добавление и изменение полей

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

// PYTHON_EXECUTOR (Pyodide Wasm)

Одна и та же операция присваивания (dict[key] = value) добавляет новое поле, если такого ключа не было, и заменяет значение, если ключ уже существовал, — Python сам решает, что нужно, по факту наличия ключа.

⚠️ Подводный камень

Обращение по ключу, которого нет в словаре, не возвращает «пусто» или None — оно ломает программу.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 7, in <module>
literature_score = student['literature']
KeyError: 'literature'
ОшибкаЧто значит для PythonЧеловеческий перевод
KeyError«Ключа 'literature' в словаре нет»В карточке ученика нет этого поля — предмет не сдавался, либо в названии ключа опечатка. Прямое обращение dict[key] требует гарантии, что ключ существует.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: .get(key, default) устроен как вопрос «а если такого ключа нет?» — вместо падения он тихо возвращает значение по умолчанию. Для обязательных полей (например, 'name' — карточка без имени бессмысленна) прямое обращение dict[key] даже полезно: KeyError сразу укажет на кривую запись. А вот для необязательных полей вроде отдельных предметов .get() — стандартный безопасный выбор.


5.2 Обход словаря: .keys(), .values(), .items()

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

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Подводный камень

Обход for x in dict по умолчанию перебирает ключи, а не значения. На отдельной карточке ученика это не бросается в глаза, но на словаре «предмет → балл» ломает расчёт.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 5, in <module>
total_score += grade
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
ОшибкаЧто значит для PythonЧеловеческий перевод
TypeError«Нельзя сложить int и str»for grade in subject_scores перебирает названия предметов (ключи), а не баллы (значения). Переменная grade на самом деле хранила строку 'math'.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: for x in dict — это сокращение для for x in dict.keys(), поведение задано языком, а не ошибка интерпретатора. Если нужны значения — .values(), если нужны и ключ, и значение вместе — .items(). Явное имя метода избавляет от догадок о том, что именно лежит в переменной цикла.


5.3 Группировка по предметам: накопление в словаре, setdefault()

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

// PYTHON_EXECUTOR (Pyodide Wasm)

setdefault(key, default) — это одна операция вместо «проверить, есть ли ключ, и если нет — создать его»: если ключа не было, он появляется со значением default и тут же возвращается; если ключ уже был, возвращается его текущее значение, ничего не перезаписывая.

💡 На заметку: В профессиональной аналитике для автосоздания списков также часто используют defaultdict(list) из встроенного модуля collections — его мы мельком коснёмся в Главе 12.

⚠️ Подводный камень

Словарь нельзя менять по составу ключей прямо во время обхода for key in dict — ни добавлять, ни удалять.

// PYTHON_EXECUTOR (Pyodide Wasm)
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 отслеживает состав ключей во время цикла и не разрешает его менять «под ногами».

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: list(scores_by_subject) создаёт отдельный список ключей — точно так же, как [:] в главе Списки создавал независимую копию списка. Цикл теперь идёт по этой копии, а сам словарь можно спокойно менять внутри тела цикла: копия уже не следит за его текущим составом.


5.4 Вложенные структуры: словарь словарей и список словарей как модель выгрузки

Зачем это нужно. Настоящая выгрузка — это обычно много учеников, и у каждого несколько полей. Такую структуру моделируют либо списком словарей (одна запись — один словарь, порядок как в файле), либо словарём словарей (быстрый доступ по ФИО без перебора).

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Подводный камень

Чем глубже вложенность, тем легче потерять счёт уровням — обратиться на один уровень глубже, чем есть на самом деле.

// PYTHON_EXECUTOR (Pyodide Wasm)
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'] к ней означает попытку взять срез строки по текстовому ключу. Сигнал: структура на самом деле на один уровень площе, чем предполагалось в коде.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: перед тем как писать 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 здесь не появляется вовсе, программа просто считает неверно:

// PYTHON_EXECUTOR (Pyodide Wasm)

Причина та же, что и с backup = original в главе Списки: empty_list — не «шаблон нового списка», а конкретный объект, и оба ключа получили ссылку на один и тот же список. Исправление — создавать новый список прямо в вызове: scores_by_subject.setdefault(subject, []), без переменной-прослойки. ⚠️ Похожая ловушка с изменяемым значением по умолчанию поджидает в главе Функции — там она прячется в аргументах функции.


Мини-кейс: свод по предметам — средний балл и доля сдавших по каждому

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

// PYTHON_EXECUTOR (Pyodide Wasm)
--- Свод по предметам ---
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()

Дальше: Множества и кортежи