Статьи
October 3, 2022

Руководство по dictionary comprehension в 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

Dictionary Comprehension — это однострочный способ создания словарей, в рамках которого могут быть использованы и условия. Грамотное использование этого инструмента делает ваш код чище и удобнее для восприятия.

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)])

А теперь рассмотрим самый простой шаблон использования dictionary comprehension в 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

Dictionary comprehension позволяют решать сложные задачи с помощью минимального количества строк кода. Более того, им можно заменять циклы for и lambda-функции. Ими можно заменить далеко не любой цикл, но любое dict comprehension может быть переписано в виде цикла.

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.

А теперь сделаем то же самое, но с помощью dictionary comprehension:

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}

Решение получилось компактным, эффективным и не менее удобным для восприятия, чем первая реализация.

Добавление условных выражений в dictionary comprehension

В процессе решения различных задач нам часто нужно использовать различные ветвления. В зависимости от обстоятельств программа должна выполнить различные операции, и сейчас мы рассмотрим пример использования условных выражений в dictionary comprehension.

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_tripleCond = {k:v for k, v in dict1.items() if v>2 if v%2 == 0 if v%3 == 0}

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

Если бы мы использовали цикл for, то код был бы таким:

dict1_tripleCond = {}

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

print(dict1_tripleCond)
# {'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'}

Вложенные dict comprehension

Вложенность — концепция программирования, при которой одна конструкция языка может содержать в себе другие конструкции. Скорее всего, вы сталкивались с вложенностью при работе с циклами или многомерными списками.

Одно dict comprehension может находиться внутри другого. Однако, в этой ситуации читабельность кода резко снизится. То же самое касается и случаев, когда словари имеют слишком сложную структуру.

Dictionary comprehension не является панацеей, поэтому иногда лучше использовать обычные вложенные циклы. Да, код будет занимать больше места, зато вы всегда сможете понять его суть, и приведенный ниже пример наглядно это демонстрирует.

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}}

Как можно заметить, иногда лучше написать более понятный код, нежели бездумно экономить строки кода.

Заключение

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

Спасибо за внимание!

Источник: Data Camp

PythonTalk в Telegram

Чат PythonTalk в Telegram

Предложить материал | Поддержать канал