Статьи

Руководство по словарным включениям в Python

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

Иногда именно словарь оказывается наиболее подходящей структурой данных для решения тех или иных задач. Они часто применяются и в Data Science, поэтому словарные включения (dictionary comprehension) будет полезным навыком для вас.

Словари в Python

Словарём в Python является набор элементов, доступ к которым осуществляется по определённому ключу, а не по индексу. Что это значит?
Представьте себе настоящий словарь. Когда вам нужно найти значение какого-либо слова, вы используете именно это слово, а не его порядковый номер. Слово — это ключ, а его определение — и есть искомое значение. Именно эта концепция заложена в словари Python.
Примечание: Ключи в словаре должны быть хэшируемыми.
Хэширование — это процесс преобразования элемента с использованием определенного вида функции. Такая функция называется «хэш-функцией» и возвращает уникальное выходное значение для уникального входного. Целые числа, числа с плавающей точкой, строки, кортежи и замороженные множества (frozensets) хэшировать можно, в то время как списки, словари и обычные множества — нет.
Вы можете инициализировать словарь в Python следующим образом:
a = {'яблоко': 'фрукт', 'свекла': 'овощ', 'торт': 'десерт'}
a['пончик'] = 'закуска'

print(a['яблоко'])
# фрукт
В коде выше мы создаем словарь с именем a, содержащий три пары ключ-значение. Тут ключи — это строки, представляющие названия товаров, а значения — строки, представляющие тип или категорию товара. Далее мы добавляем в словарь a новую пару ключ-значение, используя синтаксис a['пончик'] = 'закуска'.

Далее мы выводим значение (фрукт), связанное с ключом яблоко в словаре a.

Теперь давайте попробуем воспользоваться индексом, как мы обычно делаем это со списками:
print(a[0])

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-2-6dd185293d34> in <module>()
      2 a['пончик'] = 'закуска'
      3 
----> 4 print(a[0])
KeyError: 0
Обратите внимание, что в словаре a не существует ключа 0, поэтому мы и получили KeyError.
Элементы в рамках одного и того же словаря могут принадлежать к разным типам данных.

Вот несколько примеров базовых действий со словарями.

Изменение значения элемента

a = {'один': 1, 'два': 'два', 'три': 3.0, 'четыре': [4, 4.0]}

print(a)
# {'один': 1, 'два': 'два', 'три': 3.0, 'четыре': [4, 4.0]}
Обновим значение для ключа один:
a['один'] = 1.0

print(a)
# {'один': 1.0, 'два': 'два', 'три': 3.0, 'четыре': [4, 4.0]}

Удаление элементов из словаря

Удалим один конкретный элемент из словаря:
del a['один']

print(a)
# {'два': 'два', 'три': 3.0, 'четыре': [4, 4.0]}
Удалим все элементы из словаря:
a.clear()

print(a)
# {}
Удалим сам словарь:
del a 

print(a)
# NameError: name 'a' is not defined
Также важно помнить, что каждый ключ в словаре должен быть уникальным. Если в процессе инициализации или модификации словаря это правило было нарушено, старое значение ключа будет заменено на новое. В этом довольно легко убедиться:
a = {'один': 1, 'два': 'два', 'один': 3.0}

print(a['один'])
# 3.0

Словарные включения (dictionary comprehension) в Python

Словарные включения — это однострочный способ создания словарей, в рамках которого могут быть использованы и условия. Грамотное использование этого инструмента делает код чище и удобнее для восприятия.
Python уже содержит встроенные методы получения ключей и значений словаря: keys() и values() соответственно:
a = {'один': 1, 'два': 'два', 'три': 3.0}

print(a.keys())
print(a.values())
# dict_keys(['один', 'два', 'три'])
# dict_values([1, 'два', 3.0])
С помощью метода items() можно получить доступ сразу ко всем парам "ключ-значение" в словаре:
print(a.items())
# dict_items([('один', 1), ('два', 'два'), ('три', 3.0)])
А теперь рассмотрим самый простой шаблон использования словарных включений в Python.
dict_variable = {key:value for key, value in dictonary.items()}
Его можно усложнять, добавляя различные операции над элементами и условные выражения. Но мы начнем с простого:
numbers = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

double_numbers = {k:v*2 for k, v in numbers.items()}

print(double_numbers)
# {'a': 2, 'b': 4, 'c': 6, 'd': 8, 'e': 10}
В данном примере мы создали новый словарь double_numbers из уже существующего numbers, удвоив значения каждого его ключа.

Такой же трюк можно проделать и с самими ключами. Умножив их на 2, мы получим следующий результат:
numbers = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

double_numbers = {k*2:v for (k, v) in numbers.items()}

print(double_numbers)
# {'aa': 1, 'bb': 2, 'cc': 3, 'dd': 4, 'ee': 5}

Альтернатива циклам for

Словарные включения позволяют решать сложные задачи с помощью минимального количества строк кода. Более того, им можно заменять циклы for и lambda-функции. Ими можно заменить далеко не любой цикл, но любое словарное включение может быть переписано в виде цикла.
numbers = range(10)
new_dict_for = {}

for n in numbers:
    if n % 2==0:
        new_dict_for[n] = n ** 2
        
print(new_dict_for)
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

new_dict_comp = {n:n**2 for n in numbers if n%2 == 0}

print(new_dict_comp)
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
В этом примере мы инициализировали список целых чисел numbers от 0 до 9, а также пустой словарь new_dict_for. После этого мы заполнили словарь new_dict_for только чётными числами из списка numbers, возведя их в квадрат.

В первом случае мы использовали цикл for, а во втором — dictionary comprehension. Как можно заметить, результат получился одинаковым, но благодаря dict comprehension мы обошлись всего одной строчкой кода, в то время как реализация с циклом for потребовала целых три.
Разница между этими двумя подходами особенно явно проявляется при работе с вложенными циклами for. Когда один цикл находится внутри другого, читаемость кода резко снижается, равно как и его понятность. В то же время, более компактная запись создания словаря позволяет обойтись меньшей кровью, сохранив необходимый функционал.

Альтернатива lambda-функциям

lambda-функции — это способ создания функций, которые предполагают их однократное использование только там, где они определены. В основном используются вместе с функциями map(), filter() и reduce().
fahrenheit = {'t1': -30, 't2': -20, 't3': -10, 't4': 0}

celsius = list(map(lambda x: (float(5)/9)*(x-32), fahrenheit.values()))

celsius_dict = dict(zip(fahrenheit.keys(), celsius))

print(celsius_dict)
# {'t1': -34.44444444444444, 't2': -28.88888888888889, 't3': -23.333333333333336, 't4': -17.77777777777778}
В примере выше мы инициализировали словарь fahrenheit с четырьмя значениями температуры в градусах по шкале Фаренгейта. Наша задача — перевести градусы Фаренгейта в градусы Цельсия.

Для этого мы создали lambda-функцию перевода градусов из одной шкалы в другую, а затем прогнали через неё объекты-значения словаря fahrenheit. После этого с помощью list() мы поместили полученные значения в список celsius. Вишенка на торте — словарь celsius_dict, созданный с помощью функции dict() на основе списка celsius.

А теперь сделаем то же самое, но с помощью словарного включения:
fahrenheit = {'t1': -30, 't2': -20, 't3': -10, 't4': 0}

celsius = {k:(float(5)/9)*(v-32) for k, v in fahrenheit.items()}

print(celsius)
# {'t1': -34.44444444444444, 't2': -28.88888888888889, 't3': -23.333333333333336, 't4': -17.77777777777778}
Решение получилось компактным, эффективным и не менее удобным для восприятия, чем первая реализация.

Добавление условий в словарные включения

В процессе решения различных задач нам часто приходится иметь дело с ветвлениями. В зависимости от ситуации программа должна выполнять разные действия, и сейчас мы рассмотрим пример использования условий в словарных включениях.
dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

dict1_cond = {k:v for k, v in dict1.items() if v > 2}

print(dict1_cond)
# {'c': 3, 'd': 4, 'e': 5}
Добавив в уже известную нам конструкцию условие v > 2, мы отбросили из словаря dict1 все элементы, значение которых меньше или равно 2. Оставшиеся элементы были помещены в новый словарь dict1_cond, содержимое которого и было выведено на экран.
В целом, довольно легко. А что насчёт сразу нескольких условий? Тоже ничего сложного, просто расположите их друг за другом. Помните, что в данном случае они работают так, будто бы между ними есть логическое "И".
dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}

dict1_triple_cond = {k:v for k, v in dict1.items() if v>2 if v%2 == 0 if v%3 == 0}

print(dict1_triple_cond)
# {'f': 6}
Если бы мы использовали цикл for, то код был бы таким:
dict1_triple_cond = {}

for (k,v) in dict1.items():
    if (v >= 2 and v % 2 == 0 and v % 3 == 0):
            dict1_triple_cond[k] = v

print(dict1_triple_cond)
# {'f': 6}

if/else в dictionary comprehension

Если необходимо использовать else, просто запишем его после соответствующей конструкции if. Пример:
dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f':6}

dict1_triple_cond = {k:('even' if v%2==0 else 'odd') for k, v in dict1.items()}

print(dict1_triple_cond)
# {'a': 'odd', 'b': 'even', 'c': 'odd', 'd': 'even', 'e': 'odd', 'f': 'even'}

Вложенные словарные включения

Вложенность — концепция программирования, при которой одна конструкция языка может содержать в себе другие конструкции. Скорее всего, вы сталкивались с вложенностью при работе с циклами или многомерными списками.
Одно словарное включение может находиться внутри другого. Однако, в этой ситуации читабельность кода резко снизится. То же самое касается и случаев, когда словари имеют слишком сложную структуру.
Словарные включения не является панацеей, поэтому иногда лучше использовать обычные вложенные циклы. Да, код будет занимать больше места, зато вы всегда сможете понять его суть, и приведенный ниже пример наглядно это демонстрирует.
nested_dict = {'first':{'a': 1}, 'second':{'b': 2}}
float_dict = {outer_k: {inner_k: float(inner_v) for inner_k, inner_v 
              in outer_v.items()} for (outer_k, outer_v) 
              in nested_dict.items()}

print(float_dict)
# {'first': {'a': 1.0}, 'second': {'b': 2.0}}
Здесь мы использовали вложенное словарное включение, чтобы преобразовать целочисленные значения элементов словаря nested_dict в числа с плавающей запятой. А вот как выглядит тот же самый код, но с использованием цикла for.
nested_dict = {'first':{'a': 1}, 'second':{'b': 2}}

for outer_k, outer_v in nested_dict.items():
    for inner_k, inner_v in outer_v.items():
        outer_v.update({inner_k: float(inner_v)})
        nested_dict.update({outer_k:outer_v})

print(nested_dict)
# {'first': {'a': 1.0}, 'second': {'b': 2.0}}
Как можно заметить, иногда лучше написать более понятный код, нежели бездумно экономить строки кода.

Заключение

Теперь вы знаете, что такое словарные включения в Python, зачем нам нужно его использовать и какие преимущества есть у этого инструмента перед другими способами работы со словарями.
Спасибо за внимание!

Источник: Data Camp