Эффективный обход словарей в цикле в Python

Опубликовано: 06 июля 2020, Пн, автор: Андрей Семакин Обновлено: 06 июля 2020, Пн 5 минут

Эффективный обход словарей в цикле в 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. Не запутайтесь.

Если понравилась статья, то подпишитесь на уведомления о новых постах в блоге, чтобы ничего не пропустить!

Ссылки

тэги: python, dict, loop, for, iterate