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

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

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

Перебор словаря: keys, values и items

Три представления словаря — keys, values, items: перебираем цены в цикле for, распаковываем пары в две переменные, считаем сумму и фильтруем дешёвые позиции.

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

До сих пор мы работали со словарём по одному ключу: проверяли наличие оператором in в уроке 7, добавляли и меняли пары в уроке 5. Но склад из двадцати товаров по одному не обходят. Пора научиться проходить по всем парам сразу — для этого у словаря есть три представления: keys, values и items.

Три представления: keys, values, items

три взгляда на один словарь
stock = {'молоко': 12, 'хлеб': 5, 'сыр': 3}

print(stock.keys())
print(stock.values())
print(stock.items())
Вывод
dict_keys(['молоко', 'хлеб', 'сыр'])
dict_values([12, 5, 3])
dict_items([('молоко', 12), ('хлеб', 5), ('сыр', 3)])

Методы вернули не совсем списки, а представления: dict_keys, dict_values, dict_items. Печатаются они как списки и работают везде, где ждут последовательность: в цикле for, в функциях list(), len() и sum(). Ключи, значения и пары — три взгляда на один и тот же словарь: данные не копируются, меняется лишь то, что видно из цикла.

Цикл for по ключам

словарь в for отдаёт ключи
prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}

for name in prices:
    print(name, prices[name])
Вывод
молоко 89
хлеб 42
сыр 590

Словарь в заголовке цикла сам отдаёт ключи — писать prices.keys() не обязательно, хотя и можно. На каждом шаге значение достаётся по ключу в квадратных скобках, и весь прайс напечатан. Это родня оператору in из урока 7: оба идут по ключам. Но запись prices[name] на каждом шаге громоздкая — для пар есть путь короче: метод items() отдаёт пары-кортежи, а две переменные в заголовке цикла распаковывают каждую пару — ключ в первую, значение во вторую. А когда нужны только величины, представление values() и функция sum() считают итог без всякого цикла.

Пары с распаковкой и суммы

ключ и значение двумя переменными
prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}

for name, price in prices.items():
    print(name, '-', price, 'руб.')
Вывод
молоко - 89 руб.
хлеб - 42 руб.
сыр - 590 руб.
сумма по значениям
stock = {'молоко': 12, 'хлеб': 5, 'сыр': 3}

total = sum(stock.values())
print(total)
print(len(stock))
Вывод
20
3

Представления — это окна, а не копии

Тонкий момент, который обсуждают на собеседованиях: stock.keys() возвращает не список, а представление — «окно» в словарь. Список копирует данные, а окно ссылается на них. Разница видна сразу: если взять окно и потом добавить в словарь новую пару, в окне она появится тоже.

окно видит изменения
stock = {'молоко': 12, 'хлеб': 5}

view = stock.keys()
print(list(view))

stock['сыр'] = 8
print(list(view))
Вывод
['молоко', 'хлеб']
['молоко', 'хлеб', 'сыр']

Второй вывод длиннее первого — потому что это тот же объект-обзор, просто посмотренный позже. Практическое следствие: если окно нужно сохранить «как было», берите list(stock.keys()) — получится обычный список с копией данных. Для циклов и подсчётов разницы нет, разве что list() на пустом словаре даст список, а не пустое окно, и его можно будет спокойно менять.

Фильтр и подсчёт за один проход

Три представления словаря придуманы не просто так. С их помощью собирают вещи, которые на списках пришлось бы писать в несколько строк. Разберём три задачи на одном словаре остатков.

пары, фильтр, сумма
stock = {'молоко': 12, 'хлеб': 5, 'сыр': 8}

for name, amount in stock.items():
    print(f'{name}: {amount}')

cheap = {name: amount for name, amount in stock.items() if amount < 10}
print(cheap)
print(sum(stock.values()))
print(max(stock.values()))
Вывод
молоко: 12
хлеб: 5
сыр: 8
{'хлеб': 5, 'сыр': 8}
25
12

Первый цикл печатает пары через f-строку. Второй — собирает новый словарь из дешёвых позиций: фигурные скобки внутри цикла строят словарь, условие после if отбирает пары. Третий и четвёртый — готовые функции: sum складывает все значения, max находит наибольшее. Ни одна из этих задач не требует отдельного счётчика или ручного сложения в цикле.

Про f-строку стоит сказать отдельно. Строчный вариант print(name, '-', amount) работает и в уроке 1 мы им пользовались, но f-строка удобнее, когда значение вставляется внутрь текста: f'{name}: {amount}'. Плюс она не создаёт лишних строк при конкатенации и читается как шаблон. Словарь в f-строке тоже можно вывести целиком — f'{stock}' даст то же самое, что print(stock), только встроенную в строку.

изменение словаря во время обхода
stock = {'молоко': 12, 'хлеб': 5}

try:
    for name in stock:
        del stock[name]
except RuntimeError as e:
    print('RuntimeError:', e)
Вывод
RuntimeError: dictionary changed size during iteration

Обратите внимание: первая же итерация удаляет пару, и обход тут же падает. Никакого предупреждения заранее Python не выдаёт — ошибка прилетает в момент, когда размер таблицы изменился посреди прохода. Если в цикле меняют только значения, а не состав словаря, — всё в порядке: stock[name] = 0 размер не трогает и обход не ломает.

Сколько стоит перебор

Ещё одна вещь, которую стоит понимать про обход. Словарь отдаёт пары в порядке вставки — это не совпадение, а гарантия: начиная с Python 3.7 порядок словаря официально зафиксирован и не изменится. Именно на этом стоят многие решения — например, «первая пара в словаре» означает «первая добавленная», и на неё можно опираться.

Но если вам нужен другой порядок, придётся попросить Python явно: sorted(stock) даст ключи по алфавиту, sorted(stock.items(), key=lambda pair: pair[1]) — пары по значению. Эти вызовы создают новый список, то есть тратят память и время. На словаре из пары сотен записей это незаметно; на десятках тысяч — уже ощутимо. Практическое правило: сортируйте на выходе, а не каждый раз, когда кто-то читает словарь.

порядок вывода задаём сами
stock = {'молоко': 12, 'хлеб': 5, 'сыр': 8}

print(sorted(stock))
print(sorted(stock.items(), key=lambda pair: pair[1]))
print(sorted(stock.items(), key=lambda pair: -pair[1]))
Вывод
['молоко', 'сыр', 'хлеб']
[('хлеб', 5), ('сыр', 8), ('молоко', 12)]
[('молоко', 12), ('сыр', 8), ('хлеб', 5)]

Первый вызов сортирует ключи по алфавиту. Второй — пары по значению, по возрастанию: лямбда pair[1] говорит, что сравнивать надо по второму элементу кортежа. Третий — то же самое, но по убыванию: знак минус перед pair[1] переворачивает порядок. Подробнее про сортировку словарей — в уроке 13.

Обратный словарь: ключом стало значение

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

переворачиваем словарь
prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}

by_price = {}
for name, price in prices.items():
    by_price[price] = name

print(by_price)
print(by_price[42])
Вывод
{89: 'молоко', 42: 'хлеб', 590: 'сыр'}
хлеб

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

ЗадачаЧто писать
Нужны только ключиfor name in prices: — или for name in prices.keys():
Нужны только значенияfor amount in prices.values(): — сумма через sum(prices.values())
Нужны и ключ, и значениеfor name, price in prices.items():
Нужно отсортироватьsorted(prices.items(), key=lambda pair: pair[1])

Правило выбора простое: берите самое узкое представление, которое решает задачу. Обращение к prices[name] внутри цикла по ключам — лишний поиск по хешу на каждом шаге; парам из items() он не нужен.

Что дальше

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

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

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

d = {'a': 1, 'b': 2}

for k in d:
    print(k, d[k] * 10)
cart = {'чай': 50, 'кофе': 120}

print(sum(cart.values()))
d = {'x': 5}

for k, v in d.items():
    print(k, '=', v + 1)
Проверь себя
0 / 8

1. Что вернёт вызов stock.values()?

2. По чему идёт цикл for name in prices:, если prices — словарь?

3. Что делает запись for name, price in prices.items():?

4. Как посчитать сумму всех значений словаря prices?

5. Что будет, если добавить пару в словарь прямо внутри цикла for по нему?

6. Чем stock.keys() отличается от list(stock.keys())?

7. Как отобрать только пары со значением меньше 10?

8. Что произойдёт, если внутри цикла for по словарю изменить только значение пары, не добавляя и не удаляя ключи?

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

Дан словарь prices = {'молоко': 89, 'хлеб': 42, 'сыр': 590}. Перебери пары через items и выведи каждую строкой вида «молоко - 89», затем посчитай общую стоимость через sum по значениям.

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

Как перебрать словарь в Python?

Циклом for: for name in prices: идёт по ключам, а for name, price in prices.items(): — по парам, где ключ и значение распаковываются в две переменные. Пары идут в порядке вставки.

Урок 5: добавление и изменение пар

Что делает метод items() у словаря?

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

Как посчитать сумму значений словаря?

Функцией sum по представлению значений: sum(prices.values()). Число пар даёт len(prices). Для отсутствующих ключей в подсчётах пригодится get с нулём по умолчанию из урока про метод get.

Урок 4: метод get

Можно ли изменить словарь во время перебора?

Нет: добавление или удаление пар внутри цикла вызовет RuntimeError: dictionary changed size during iteration. Сначала соберите нужные ключи в отдельный список, а изменения применяйте после цикла.

Что вернёт stock.keys() — список или что-то другое?

Представление dict_keys, а не список. Это «окно» в словарь: если позже добавить пару, в окне она тоже появится. Список с копией данных получается через list(stock.keys()), а в циклах и подсчётах разницы между ними обычно нет.

Как отфильтровать словарь, оставив только дешёвые позиции?

Через dict comprehension: {name: amount for name, amount in stock.items() if amount < 10}. Фигурные скобки строят новый словарь, условие после if отбирает пары. Подробнее о comprehensions — в уроке 15.

Урок 15: dict comprehension

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

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

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

TelegramVK

Что дальше

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

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

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