Эффективный обход словарей в цикле в Python
Словарь (dictionary, dict) — это ассоциативный массив, который позволяет сохранять значения по ключам.
Это очень важная, даже можно сказать основополагающая структура данных,
которая используется в Python под капотом буквально повсюду:
модули, классы, объекты, locals()
, globals()
— все это так или иначе
работает лишь благодаря словарям.
Кроме того, словарь отлично подходит для решения множества прикладных задач, обладает хорошей вычислительной сложностью операций, так что и в вашем коде, наверняка, словари будут встречаться достаточно часто.
В Python большое внимание уделяется циклам. Правильно написанный заголовок цикла содержит много ценной информации: по чему итерируемся и какие данные будут использоваться в теле цикла. Это помогает читателю понять (или хотя бы предположить), что именно будет производиться в теле цикла, даже не смотря в него. Неправильно написанный цикл, который не выражает напрямую задумку автора, наоборот, сбивает читателя с толку и заставляет читать код целиком, возможно, даже не один раз.
Есть несколько способов обойти словарь в цикле. Очень важно научиться выбирать наиболее подходящий.
Что будет если просто попытаться обойти словарь в цикле?
Объявим словарь с отношением различных валют к российскому рублю, который нам по какой-то причине нужно обойти:
currencies = {"rub": 1, "usd": 69.78, "eur": 78.28}
Самый очевидный вариант обхода словаря — это попытаться напрямую запустить
цикл for
по объекту словаря, так же как мы делаем это со списками,
кортежами, строками и любыми другими итерируемыми объектами.
for something in currencies:
print(something)
Словарь и правда поддерживает протокол итераций, но словарь не так прост, как другие объекты, которые мы упомянули выше. Словарь состоит из нескольких частей, ведь словарь — это отношение между ключами и значениями. Получается, что теоретически цикл по словарю может получать либо ключи, либо значения, либо пары (ключ, значение). Попробуете угадать, что же именно выведет код выше?
А выведет он следующее:
rub
usd
eur
То есть обход словаря в цикле будет возвращать только ключи этого словаря.
Пожалуй, задать такое поведение по умолчанию — это очень логичное решение со стороны разработчиков Python. Было бы намного внезапнее, если бы цикл по словарю получал значения. Вариант с кортежами (ключ, значение) в качестве поведения по умолчанию мне кажется не таким уж плохим, но имеем то, что имеем.
Есть куча задач, в которых нужно обойти лишь ключи словаря, и это отличное решение для таких задач. У этого способа есть один крупный недостаток: нужно знать как работают словари. По коду совершенно неясно, что будет обходиться в цикле — ключи, значения или пары, а читатель может либо этого не знать, либо забыть, и в итоге неправильно интерпретировать код. Поэтому во избежание неоднозначности даже для обхода ключей словаря я рекомендую использовать следующий способ.
Как обойти в цикле ключи словаря?
Давайте представим, что нам нужно нарисовать какую-нибудь таблицу с валютами, и для создания шапки этой таблицы нужно получить список всех валют. Значения словаря нас не интересуют, только ключи.
У словаря есть метод .keys()
,
который возвращает представление словаря (dict view), возвращающее ключи.
Что такое представление словаря? Это некий объект, который предоставляет доступ к данным в словаре, либо к части этих данных, и работает по следующим принципам:
- не копирует содержимое словаря, а обращается к нему динамически, на больших словарях это здорово экономит память и улучшает скорость работы программы;
- если словарь изменяется, то эти изменения автоматически становятся доступными и через представление словаря;
- не является списком, не поддерживает извлечение элементов по индексам;
- является итерируемым объектом, можно использовать в циклах сколько угодно раз.
Создадим такое представление словаря по ключам:
dict_keys = currencies.keys()
print(dict_keys)
# dict_keys(['rub', 'usd', 'eur'])
Давайте добавим новый ключ в словарь:
currencies["jpy"] = 0.65
print(dict_keys)
# dict_keys(['rub', 'usd', 'eur', 'jpy'])
Как видите, созданное ранее представление словаря обновилось автоматически, когда обновился его словарь.
Обратите внимание, что представление словаря — это не список, а совершенно другой объект. Представление словаря не поддерживает извлечение значений по индексам:
dict_keys[0]
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'dict_keys' object is not subscriptable
Зато представление словаря является итерируемым объектом и его без проблем можно обходить при помощи цикла:
for key in currencies.keys():
print(key)
# rub
# usd
# eur
# jpy
Результат тот же самый, что и в предыдущем способе обхода словаря, но в этот раз явно видно, что в цикле будут обрабатываться только ключи словаря.
Обратите внимание, что если в цикле вам нужны не только ключи словаря, но и значения, то обходить словарь таким образом — не самое эффективное решение. Смотрите дальше, как можно обойти словарь, чтобы получать и ключи, и значения.
Как обойти в цикле значения словаря?
По аналогии с ключами, из словаря можно извлечь только значения, без ключей.
Это делается через метод словарей
.values()
,
который возвращает представление словаря, содержащее только значения.
Это представление работает по тем же правилам,
что и возвращаемое методом .keys()
.
Вот как можно обойти в цикле только значения словаря, без ключей:
for value in currencies.values():
print(value)
# 1
# 69.78
# 78.28
# 0.65
По значениям словаря уже невозможно получить ключи (ну, вообще можно попытаться, но для этого потребуется полный перебор словаря, и не факт, что ключи будут восстановлены правильно). Этот способ подойдёт только если в цикле используются исключительно значения словаря, а ключи не нужны.
Как обойти в цикле и ключи, и значения словаря?
Пожалуй, это самый распространённый случай. Во многих задачах, где выполняется обход словаря, в цикле используются и ключи, и соответствующие им значения.
Специально для этого у словарей есть метод
.items()
,
который возвращает представление словаря, содержащее кортежи из двух элементов,
вида (ключ, значение).
Это представление работает по точно таким же правилам,
как .keys()
и .values()
. Единственное отличие этого представления от
предыдущих состоит в том, что оно возвращает не единичные значения,
а кортежи из двух значений.
for item in currencies.items():
# item — это кортеж (ключ, значение)
print(item[0], item[1])
# rub 1
# usd 69.78
# eur 78.28
# jpy 0.65
В Python есть возможность распаковывать итерируемые объекты, такие как кортежи, в различные переменные. Давайте на примере посмотрим как это работает:
point = (1, 2, 3)
x, y, z = point
print(x)
# 1
print(y)
# 2
print(z)
# 3
Таким образом можно распаковывать последовательности любого размера. Это намного проще, чем извлекать значения по индексам и присваивать в отдельные переменные. Этот приём можно использовать практически в любом месте программы, в том числе и в заголовке цикла.
Вот так можно обойти ключи и значения словаря, сохраняя ключ и значение в разные переменные прямо в заголовке цикла при помощи распаковки кортежа:
for key, value in currencies.items():
print(key, value)
# rub 1
# usd 69.78
# eur 78.28
# jpy 0.65
Заключение
При обходе словаря стоит руководствоваться следующей логикой:
- если в цикле используются и ключи, и значения словаря,
то нужно использовать метод
.items()
; - если в цикле используются только значения словаря,
а ключи не важны, то нужно использовать метод
.values()
; - если в цикле нужны ключи словаря и ничего больше,
то нужно использовать метод
.keys()
.
Идеоматичный код проще читается и, как правило, работает быстрее.
Посмотрите запись классического выступления Реймонда Хеттингера, где он рассказывает про написание идеоматичного код. Много внимания уделяется циклам и словарям.
Обратите внимание, что это запись выступления от 2013 года, когда ещё вовсю был в ходу Python 2. В выступлении часто сравнивается Python 2 и Python 3. Не запутайтесь.
Если понравилась статья, то подпишитесь на уведомления о новых постах в блоге, чтобы ничего не пропустить!
Ссылки
- очень подробный разбор по обходу словарей на RealPython;
- документация про представления словарей;
- и, конечно же, посмотрите выступление Реймонда Хеттингера, обожаю этого чувака.