Shanraq.org Shanraq.org
Словарь: связь вместо надежды на порядок
IT

Python: от данных до своей сводки Урок 6 из 56

Словарь: связь вместо надежды на порядок

Шестой урок курса по Python. Цены по названию: get вместо ошибки, in вместо перебора, items() как готовая таблица. Измерено: обращение к несуществующему ключу — это KeyError, а не пустота; Counter считает корзину одной строкой, а defaultdict сам подставляет ноль там, где обычный словарь падает.

Зачем это нужно

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

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

Сразу целиком

Файл sozdik.py. Запуск: python sozdik.py из окружения.

Программа длинная — шестьдесят строк. Обязательное — сам словарь, get, in и items(). Counter и defaultdict в конце идут сверх того: их достаточно узнать в лицо, в задании они не нужны.

"""Урок 6: словарь — связь между названием и значением.

В прошлом уроке названия и цены лежали в двух списках, и порядок в них
приходилось держать в уме. Словарь заменяет эту надежду связью.
"""

from collections import Counter, defaultdict

# Словарь: ключ — то, по чему ищут, значение — то, что находят.
prices = {"хлеб": 280, "молоко": 620, "масло": 1890, "соль": 90}

print("== словарь целиком и по ключу")
print("весь словарь:", prices)
print("сколько записей:", len(prices))
print("цена молока:", prices["молоко"])

print()
print("== ключа нет — и это ошибка, а не пустота")
try:
    print(prices["сахар"])
except KeyError as err:
    print("prices[\"сахар\"] →", type(err).__name__, "—", err)
print("с get() безопасно:", prices.get("сахар"))
print("get() с умолчанием:", prices.get("сахар", 0), "тенге")
print("есть ли соль:", "соль" in prices)
print("есть ли сахар:", "сахар" in prices)

print()
print("== словарь меняют")
prices["сахар"] = 450
print("добавили сахар:", prices)
prices["хлеб"] = 310
print("хлеб подорожал:", prices)
removed = prices.pop("соль")
print("убрали соль за", removed, "→", prices)

print()
print("== что внутри: ключи, значения, пары")
print("ключи:   ", list(prices.keys()))
print("значения:", list(prices.values()))
print("пары:    ", list(prices.items()))
print("сумма всех цен:", sum(prices.values()))
print("самое дорогое:", max(prices, key=prices.get))

print()
print("== словарь года и значения — то, что мы уже получали")
cpi = {2010: 100.0, 2015: 137.8, 2020: 202.0, 2025: 348.1}
print("индекс цен по годам:", cpi)
print("во сколько раз с 2010:", round(cpi[2025] / cpi[2010], 2))
print("отсортировано по годам:", sorted(cpi.items()))

print()
print("== посчитать, чего сколько: Counter")
basket = ["хлеб", "молоко", "хлеб", "масло", "хлеб", "молоко"]
counts = Counter(basket)
print("что в корзине:", counts)
print("хлеба:", counts["хлеб"], "| сахара:", counts["сахар"], "← у Counter нет ошибки")
print("два самых частых:", counts.most_common(2))

print()
print("== defaultdict: словарь, у которого значение появляется само")
plain = {}
try:
    plain["хлеб"] += 1
except KeyError as err:
    print("обычный словарь →", type(err).__name__, "—", err)

counted = defaultdict(int)
counted["хлеб"] += 1
counted["хлеб"] += 1
print("defaultdict сработал:", dict(counted))

Вывод:

== словарь целиком и по ключу
весь словарь: {'хлеб': 280, 'молоко': 620, 'масло': 1890, 'соль': 90}
сколько записей: 4
цена молока: 620

== ключа нет — и это ошибка, а не пустота
prices["сахар"] → KeyError — 'сахар'
с get() безопасно: None
get() с умолчанием: 0 тенге
есть ли соль: True
есть ли сахар: False

== словарь меняют
добавили сахар: {'хлеб': 280, 'молоко': 620, 'масло': 1890, 'соль': 90, 'сахар': 450}
хлеб подорожал: {'хлеб': 310, 'молоко': 620, 'масло': 1890, 'соль': 90, 'сахар': 450}
убрали соль за 90 → {'хлеб': 310, 'молоко': 620, 'масло': 1890, 'сахар': 450}

== что внутри: ключи, значения, пары
ключи:    ['хлеб', 'молоко', 'масло', 'сахар']
значения: [310, 620, 1890, 450]
пары:     [('хлеб', 310), ('молоко', 620), ('масло', 1890), ('сахар', 450)]
сумма всех цен: 3270
самое дорогое: масло

== словарь года и значения — то, что мы уже получали
индекс цен по годам: {2010: 100.0, 2015: 137.8, 2020: 202.0, 2025: 348.1}
во сколько раз с 2010: 3.48
отсортировано по годам: [(2010, 100.0), (2015, 137.8), (2020, 202.0), (2025, 348.1)]

== посчитать, чего сколько: Counter
что в корзине: Counter({'хлеб': 3, 'молоко': 2, 'масло': 1})
хлеба: 3 | сахара: 0 ← у Counter нет ошибки
два самых частых: [('хлеб', 3), ('молоко', 2)]

== defaultdict: словарь, у которого значение появляется само
обычный словарь → KeyError — 'хлеб'
defaultdict сработал: {'хлеб': 2}

Разбор

Ключ и значение

prices = {"хлеб": 280, "молоко": 620, "масло": 1890, "соль": 90}

Фигурные скобки, пары через двоеточие. Слева ключ — то, по чему ищут; справа значение — то, что находят. prices["молоко"] даёт 620 сразу, не перебирая остальные.

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

Ключа нет — это ошибка

prices["сахар"] → KeyError — 'сахар'

Python не возвращает пустоту молча: нет ключа — есть исключение. Это правильно, потому что «нет данных» и «ноль» — разные вещи, и путать их в расчётах опасно.

Когда отсутствие ключа — нормальная ситуация, спрашивают через get:

с get() безопасно: None
get() с умолчанием: 0 тенге

get(ключ) вернёт None, get(ключ, 0) — ноль. А если нужно просто узнать, есть ли ключ, — оператор in: "соль" in prices даёт True.

Образ. Телефонная книга. Спросить «телефон Асхата» и получить ошибку «такого нет» честнее, чем получить пустой номер и позвонить в никуда.

Словарь меняют

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

Обратите внимание на порядок в выводе: он сохраняется — тот, в котором добавляли. Это гарантировано начиная с Python 3.7 и удобно, но полагаться на порядок как на смысл не стоит: словарь ищут по ключу, а не по месту.

Ключи, значения и пары

ключи:    ['хлеб', 'молоко', 'масло', 'сахар']
значения: [310, 620, 1890, 450]
пары:     [('хлеб', 310), ('молоко', 620), ('масло', 1890), ('сахар', 450)]

.items() даёт пары — те самые кортежи из прошлого урока. Список пар — это уже таблица, и именно так словарь превращается в данные, с которыми работают дальше.

sum(prices.values()) считает сумму всех цен. max(prices, key=prices.get) возвращает ключ с наибольшим значением — то есть название самого дорогого товара, а не саму цену.

Год как ключ

cpi = {2010: 100.0, 2015: 137.8, 2020: 202.0, 2025: 348.1}

Это то, что наша программа из первого урока получала от Всемирного банка: индекс цен по годам. Теперь понятно, чем он был — словарём, где ключ год, а значение индекс.

cpi[2025] / cpi[2010] даёт те самые 3.48. А sorted(cpi.items()) выстраивает пары по годам — данные, готовые к графику.

Counter: посчитать, чего сколько

что в корзине: Counter({'хлеб': 3, 'молоко': 2, 'масло': 1})

Counter из модуля collections принимает список и возвращает словарь «значение → сколько раз встретилось». Отдельно удобно, что у него нет ошибки для отсутствующего ключа: counts["сахар"] даёт 0.

most_common(2) возвращает две самые частые пары. Когда дойдём до текстов и логов, это будет первой командой, которую вы наберёте.

defaultdict: значение появляется само

обычный словарь → KeyError — 'хлеб'
defaultdict сработал: {'хлеб': 2}

plain["хлеб"] += 1 для обычного словаря — ошибка: чтобы прибавить единицу, надо сначала что-то иметь. defaultdict(int) в такой ситуации сам подставляет ноль, и строка работает.

Это не магия, а правило: defaultdict принимает функцию, которая делает значение по умолчанию. int даёт 0, list — пустой список. Второй вариант понадобится, когда будем складывать строки по группам.

Карта урока

Карта урока: ключ, значение и счётчик

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему обращение к несуществующему ключу — ошибка, а не пустое значение?
  2. Что возвращает max(prices, key=prices.get) — цену или название?
  3. Чем defaultdict(int) отличается от обычного словаря в строке d["хлеб"] += 1?

Разминка

Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает эта строка?

prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар"), "соль" in prices, len(prices))

2. Заполните пропуск. Вместо ... поставьте умолчание, при котором цена отсутствующего товара считается нулём.

prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар", ...))

3. Почините. Программа падает на товаре, которого нет в справочнике. Сделайте так, чтобы она ответила 0.

prices = {"хлеб": 280, "соль": 90}
print(prices["сахар"])

Задание

Обязательное. Дано:

prices = {"хлеб": 280, "молоко": 620, "масло": 1890, "соль": 90}

Напечатайте: сколько записей; цену молока по ключу; цену сахара через get без умолчания и с умолчанием 0; есть ли в справочнике сахар и соль. Затем добавьте сахар за 450 и напечатайте, сколько стало записей и на какую сумму; самую дорогую цену; список названий. Циклов не пишите.

Ожидаемый вывод:

записей: 4
цена молока: 620
цена сахара: None
цена сахара с умолчанием: 0 тенге
сахар есть: False | соль есть: True
после добавления: 5 записей, сумма: 3330
самая дорогая цена: 1890
названия: ['хлеб', 'молоко', 'масло', 'соль', 'сахар']

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

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

По желанию.

  • Посчитайте Counter по списку своих покупок за неделю и выведите три самых частых.
  • Возьмите словарь cpi из урока и посчитайте, во сколько раз выросли цены с 2015 по 2025 год.
  • Попробуйте сделать ключом список — получите ошибку и прочитайте, что в ней написано.

Куда это встанет в проекте

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

Долги. Мы всё ещё не умеем обойти словарь по всем ключам — для этого нужен цикл, следующий урок. Данные по-прежнему живут только в памяти: закрыли программу — и их нет. И мы пока не защищаемся от того, что источник ответит пустотой.

Ответы

Показать ответы

На вопросы

  1. Потому что «нет данных» и «ноль» — разные вещи, и подставлять второе вместо первого опасно: расчёт получится тихо неверным. Когда отсутствие ключа нормально, для этого есть get с умолчанием.
  2. Название: max перебирает ключи, а key=prices.get говорит, по какому значению их сравнивать. Чтобы получить цену, нужно max(prices.values()).
  3. У обычного словаря d["хлеб"] += 1 требует, чтобы ключ уже был, иначе KeyError. defaultdict(int) подставляет 0 в момент первого обращения, и прибавление работает.

К разминке

  1. None True 2. get для отсутствующего ключа возвращает None вместо ошибки, in отвечает True или False, а len считает записи, а не буквы.
None True 2
  1. 0. Второй аргумент get — это ответ на случай «ключа нет». Он же место, где решают, чем считать отсутствие: нулём здесь, а в расчёте средней цены — уже нет.
prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар", 0))
0
  1. KeyError: 'сахар'. Квадратные скобки требуют ключ и падают, если его нет; get спрашивает и возвращает умолчание. Первое годится, когда ключ обязан быть, второе — когда его может не быть:
prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар", 0))
0

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

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

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.