Генераторы словарей в Python

Словари (или dict) в Python — это, как и списки, способ хранения элементов. Но если в списке вы можете обращаться к элементам по их индексам, то в словаре доступ к элементам осуществляется с помощью ключей.

Друзья, подписывайтесь на наш телеграм канал Pythonist. Там еще больше туториалов, задач и книг по Python.

Вы создаете фиксированный ключ, назначаете ему значение и в дальнейшем получаете доступ к этому значению по ключу. В словаре вы имеете дело с парой «ключ-значение», которая иногда является более подходящей структурой данных для решения многих задач, чем простой список.

Работать со словарями питонисту приходится часто, особенно если он занимается наукой о данных. Поэтому сегодня мы поговорим о важном навыке, полезном при работе со словарями. Мы поговорим про генераторы словарей в Python.

В этой статье:

  • Что такое словарь в Python и как его можно эффективно использовать
  • Генераторы словарей в Python: в чем их важность и как они могут служить альтернативой циклам for и лямбда-функциям
  • Добавление условий в генератор. Мы будем работать с условиями if, несколькими условиями if, а также операторами if-else
  • Что такое генератор вложенного словаря, как вы можете его использовать и как его можно потенциально переписать с помощью циклов for.

Итак, давайте приступать!

Словари в Python

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

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

Примечание. Ключи в словаре должны быть хешируемыми.

Хеширование — это процесс прогона объекта через определенную функцию. Эта функция называется «хеш-функцией». Она возвращает уникальный результат для уникального входного значения.

Целые числа, числа с плавающей запятой, строки, кортежи и frozenset можно хэшировать. В то время как списки, словари и множества (кроме frozenset), не являются таковыми. Хеширование — довольно сложная тема, и это только основная концепция хеширования.

Инициализировать словарь в Python можно следующим образом:

a = {'apple': 'fruit', 'beetroot': 'vegetable', 'cake': 'dessert'}
a['doughnut'] = 'snack'
print(a['apple'])
# fruit

Но если вы обратитесь к элементу не по ключу (print(a['apple'])), а по индексу (print(a[0])), вы получите ошибку:

---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

<ipython-input-9-00d4a978143a> in <module>()
----> 1 print(a[0])


KeyError: 0

Мы получили сообщение об ошибке типа KeyError, поскольку ключа 0 не существует.

[python_ad_block]

Элементы словаря могут иметь любой тип данных. Посмотрите еще несколько примеров словарей, чтобы лучше это понять:

# Создание и вывод на экран словаря
a = {'one': 1, 'two': 'to', 'three': 3.0, 'four': [4,4.0]}
print(a)
# {'four': [4, 4.0], 'two': 'to', 'three': 3.0, 'one': 1}

# Добавление элемента в словарь (обновление словаря)
a['one'] = 1.0 
print(a)
# {'four': [4, 4.0], 'two': 'to', 'three': 3.0, 'one': 1.0}

# Удаление одного элемента
del a['one'] 
print(a)
# {'four': [4, 4.0], 'two': 'to', 'three': 3.0}

# Удаление всех элементов словаря
a.clear()
print(a)
# {}

# Удаление самого словаря
del a 
print(a)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-12-701c9d6596da> in <module>()
      1 del a #Deletes the dictionary
----> 2 print(a)


NameError: name 'a' is not defined

Важно помнить, что ключи в словаре должны быть уникальными, дубликаты не допускаются. Но в случае дублирования ключей Python не выдаст ошибку. Вместо этого он сочтет верным последний экземпляр ключа и просто проигнорирует первую пару ключ-значение. Посмотрите сами, как это работает:

sweet_dict = {'a1': 'cake', 'a2':'cookie', 'a1': 'icecream'}
print(sweet_dict['a1'])
# icecream

Генераторы словарей в Python

Генераторы словарей (dict comprehension) — это метод преобразования одного словаря в другой. Во время этого преобразования элементы исходного словаря, соответствующие заданным условиям, включаются в новый словарь. При этом каждый элемент может быть преобразован по мере необходимости.

Хороший генератор (списка, словаря, множества) может сделать ваш код более точным и, следовательно, более легким для чтения. При этом главное — не позволять им становиться слишком сложными. У вас не должно возникать проблем с пониманием того, что они делают. Придерживайтесь принципа «Хорошо то, что легко читается».

Работа с генераторами словарей в Python базируется на возможности доступа к ключам и значениям. А для доступа к ним в Python есть специальные строенные методы:

dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4}

# Поместить все ключи `dict1` в список и вернуть этот список
dict1.keys()
# dict_keys(['c', 'd', 'a', 'b'])

# Поместить все значения, сохраненные в `dict1`, в список и вернуть этот список
dict1.values()
# dict_values([3, 4, 1, 2])

Итак, теперь вы знаете, как получить доступ ко всем ключам и их значениям в словаре. Вы также можете получить доступ ко всем парам ключей и значений в словаре, используя метод items():

dict1.items()
# dict_items([('c', 3), ('d', 4), ('a', 1), ('b', 2)])

Общий шаблон генераторов словарей в Python:

dict_variable = {key:value for (key,value) in dictonary.items()}

Это самый простой вариант генератора. При добавлении условий код усложнится.

Давайте начнем с простого примера:

dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# Удвоить каждое значение в словаре
double_dict1 = {k:v*2 for (k,v) in dict1.items()}

print(double_dict1)
# {'e': 10, 'a': 2, 'c': 6, 'b': 4, 'd': 8}

В приведенном выше коде генератор создает новый словарь double_dict1 из словаря dict1, просто удваивая каждое значение в нем.

Менять можно не только значения, но и ключи. Например, давайте создадим такой же словарь, что и выше, но изменим имена ключей. Выглядеть это будет так:

dict1_keys = {k*2:v for (k,v) in dict1.items()}
print(dict1_keys)
# {'dd': 4, 'ee': 5, 'aa': 1, 'bb': 2, 'cc': 3}

Зачем использовать генераторы словарей?

Генератор словарей — мощная концепция, которую можно использовать для замены циклов и лямбда-функций. При этом не все циклы for могут быть записаны как генератор словаря, но любой генератор можно переписать с использованием цикла for.

Рассмотрим следующую задачу. Вы хотите создать новый словарь, где ключами будут четные числа в диапазоне от 0 до 10, а значениями — квадраты этих чисел.

Давайте посмотрим, как можно решить эту проблему, используя цикл for и генератор словарей:

numbers = range(10)
new_dict_for = {}

# Добавляем значения в `new_dict` с помощью цикла for 
for n in numbers:
    if n%2==0:
        new_dict_for[n] = n**2

print(new_dict_for)
# {0: 0, 8: 64, 2: 4, 4: 16, 6: 36}

# Используем генератор словаря
new_dict_comp = {n:n**2 for n in numbers if n%2 == 0}

print(new_dict_comp)
# {0: 0, 8: 64, 2: 4, 4: 16, 6: 36}

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

Циклы for используются для повторения определенной операции или блока инструкций в программе заданное количество раз. Однако вложенные циклы for (цикл for внутри другого цикла for) могут стать запутанными и сложными. В таких ситуациях лучше подходят генераторы словарей. Они могут упростить чтение и понимание кода.

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

Лямбда-функции — это небольшие анонимные функции. Т.е. функции без имени. Они являются одноразовыми функциями, которые нужны только там, где были созданы. Лямбда-функции в основном используются в сочетании с функциями filter(), map() и reduce().

Давайте рассмотрим применение лямбда-функции вместе с функцией map(). Допустим, нам нужно преобразовать словарь с температурой по Фаренгейту в словарь с температурой по Цельсию.

# Инициализируем словарь  `fahrenheit` 
fahrenheit = {'t1':-30, 't2':-20, 't3':-10, 't4':0}

# Получаем соответствующие значения `celsius`
celsius = list(map(lambda x: (float(5)/9)*(x-32), fahrenheit.values()))

# Создаем словарь `celsius`
celsius_dict = dict(zip(fahrenheit.keys(), celsius))

print(celsius_dict)
# {'t2': -28.88888888888889, 't3': -23.333333333333336, 't1': -34.44444444444444, 't4': -17.77777777777778}

Давайте разберем код. Во-первых, вам нужно определить математическую формулу, которая выполняет преобразование из градусов Фаренгейта в градусы Цельсия. В коде это делается с помощью лямбда-функции. Затем вы передаете эту функцию в качестве аргумента функции map(), которая применяет операцию к каждому элементу в списке fahrenheit.values().

Помните функцию values()? Она возвращает список, содержащий значения, хранящиеся в словаре.

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

В Python есть встроенная функция zip(), которая перебирает элементы итераторов и объединяет их. Подробнее о функции zip() можно прочитать в статье «Используем zip() для парной итерации».

В приведенном выше примере функция zip() объединяет элементы из fahrenheit.keys() и списка градусов Цельсия, создавая пары ключ-значение, которые вы можете поместить в словарь с помощью функции dict, что и является желаемым результатом.

Теперь давайте попробуем решить ту же задачу, используя генератор словаря:

# Инициализируем словарь  `fahrenheit`
fahrenheit = {'t1': -30,'t2': -20,'t3': -10,'t4': 0}

# Получаем соответствующие значения в градусах Цельсия и создаем новый словарь `celsius` 
celsius = {k:(float(5)/9)*(v-32) for (k,v) in fahrenheit.items()}

print(celsius_dict)
# {'t2': -28.88888888888889, 't3': -23.333333333333336, 't1': -34.44444444444444, 't4': -17.77777777777778}

Как видите, с использованием генератора словаря задача может быть решена с помощью одной строки кода. А в первой реализации потребовался двухэтапный процесс и понимание работы трех функций (лямбда, map() и zip()).

Кроме того, благодаря генератору словаря решение становится интуитивно понятным и простым для чтения. Следовательно, генераторы словарей могут служить хорошей альтернативой лямбда-функциям.

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

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

Условие if

Предположим, вам нужно создать новый словарь из имеющегося, но с элементами, большими 2. Это означает, что вам нужно добавить условие к исходному шаблону, который вы видели выше. Сделать это можно следующим образом:

dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# Проверка, больше ли элемент, чем 2
dict1_cond = {k:v for (k,v) in dict1.items() if v>2}

print(dict1_cond)
# {'e': 5, 'c': 3, 'd': 4}

Это не так сложно! Но что, если у вас несколько условий?

Несколько условий if

Допустим, что нам нужно не только получить элементы, большие двух, но и одновременно проверить, кратны ли они двум.

dict1_doubleCond = {k:v for (k,v) in dict1.items() if v>2 if v%2 == 0}
print(dict1_doubleCond)
# {'d': 4}

Мы просто добавляем условия в генератор одно за другим. Однако вам нужно быть осторожным с тем, что вы пытаетесь сделать в задаче. Помните, что последовательные операторы if работают так, как если бы между ними были логические and.

Давайте посмотрим еще один пример с тремя условными операторами:

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

Добавлять условия if-else в генератор словаря тоже просто. Убедитесь сами:

dict1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f':6}

# Определяем четные и нечетные элементы
dict1_tripleCond = {k:('even' if v%2==0 else 'odd') for (k,v) in dict1.items()}

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

Вложенные генераторы словарей

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

Точно так же словари могут быть вложены друг в друга, и, следовательно, их генераторы тоже. Давайте посмотрим, что это означает:

nested_dict = {'first':{'a':1}, 'second':{'b':2}}
float_dict = {outer_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': {1.0}, 'second': {2.0}}

Это пример вложенного словаря. nested_dict — это словарь с ключами first и second, значениями которых являются другие словари. Код работает со значениями внутреннего словаря и преобразует их в значения с плавающей запятой, а затем объединяет внешние ключи с новыми внутренними значениями с плавающей запятой в новый словарь.

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

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

Обратите внимание, что вы можете переписать приведенный выше фрагмент кода, используя вложенный цикл 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. Мы повторили то, что знаем о словарях, и познакомились с концепцией генераторов. Кроме того, мы рассмотрели, где генераторы могут быть полезны, а где их лучше заменить циклом for. Теперь вы готовы эффективно работать с ними.

Надеемся, данная статья была вам полезна! Успехов в написании кода!

Перевод статьи «Python Dictionary Comprehension Tutorial».

Прокрутить вверх