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 — пустой список. Второй вариант понадобится, когда будем складывать строки по группам.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему обращение к несуществующему ключу — ошибка, а не пустое значение?
- Что возвращает
max(prices, key=prices.get)— цену или название? - Чем
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 год. - Попробуйте сделать ключом список — получите ошибку и прочитайте, что в ней написано.
Куда это встанет в проекте
Сводка получает свою главную структуру: год → значение. Именно так мы будем хранить курс тенге по дням и индекс цен по годам, пока не переедем в базу.
Долги. Мы всё ещё не умеем обойти словарь по всем ключам — для этого нужен цикл, следующий урок. Данные по-прежнему живут только в памяти: закрыли программу — и их нет. И мы пока не защищаемся от того, что источник ответит пустотой.
Ответы
Показать ответы
На вопросы
- Потому что «нет данных» и «ноль» — разные вещи, и подставлять второе вместо первого опасно: расчёт получится тихо неверным. Когда отсутствие ключа нормально, для этого есть
getс умолчанием. - Название:
maxперебирает ключи, аkey=prices.getговорит, по какому значению их сравнивать. Чтобы получить цену, нужноmax(prices.values()). - У обычного словаря
d["хлеб"] += 1требует, чтобы ключ уже был, иначеKeyError.defaultdict(int)подставляет0в момент первого обращения, и прибавление работает.
К разминке
None True 2.getдля отсутствующего ключа возвращаетNoneвместо ошибки,inотвечаетTrueилиFalse, аlenсчитает записи, а не буквы.
None True 2
0. Второй аргументget— это ответ на случай «ключа нет». Он же место, где решают, чем считать отсутствие: нулём здесь, а в расчёте средней цены — уже нет.
prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар", 0))
0
KeyError: 'сахар'. Квадратные скобки требуют ключ и падают, если его нет;getспрашивает и возвращает умолчание. Первое годится, когда ключ обязан быть, второе — когда его может не быть:
prices = {"хлеб": 280, "соль": 90}
print(prices.get("сахар", 0))
0
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.