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

Генераторы словарей во многом аналогичны генераторам списков. В простейшем случае они состоят из выражения, цикла и итерируемого объекта, которые заключаются в фигурные скобки:

>>> {x: x**2 + 1 for x in range(5)}
{0: 1, 1: 2, 2: 5, 3: 10, 4: 17}

Давайте сразу обратим внимание на выражение x: x**2, которое на самом деле состоит из двух выражений: первое x - это выражение которое создает ключи элементов, второе x**2 - создает значения элементов. Казалось бы, что благодаря этому можно использовать две разные переменные и создавать очень сложные словари, но из-за того что добавление элементов с одинаковыми ключами приводит к перезаписи значений, подобные трюки не получаются:

>>> {x: y for x in 'ABC' for y in 'XYZ'}
{'A': 'Z', 'B': 'Z', 'C': 'Z'}

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

>>> {x: 'Z' for x in 'ABC'}
{'A': 'Z', 'B': 'Z', 'C': 'Z'}

А еще лучше для получения данного результата воспользоваться методом .fromkeys():

>>> {}.fromkeys('ABC', 'Z')
{'A': 'Z', 'B': 'Z', 'C': 'Z'}

С другой стороны, если все-таки нужно использовать две переменные, то можно воспользоваться механизмом распаковки кортежей:

>>> a = list(zip('ABC', range(3)))
>>> a
[('A', 0), ('B', 1), ('C', 2)]
>>>
>>> {x: y for x, y in a}
{'A': 0, 'B': 1, 'C': 2}
>>>
>>> # или тоже самое, но в одну строчку:
>>> {x: y for x, y in zip('ABC', range(3))}
{'A': 0, 'B': 1, 'C': 2}

Так же следует помнить, что ключами могут быть только неизменяемые (хешируемые) объекты:

>>> # неизменяемые кортежи могут быть ключами:
>>> {(x, y): x + y for x, y in zip('ABC', 'XYZ')}
{('A', 'X'): 'AX', ('B', 'Y'): 'BY', ('C', 'Z'): 'CZ'}
>>>
>>> # а изменяемые списки ключами быть не могут:
>>> {[x, y]: x + y for x, y in zip('ABC', 'XYZ')}
Traceback (most recent call last):
TypeError: unhashable type: 'list'

Вложенные генераторы

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

>>> {x: [y for y in range(x, x + 3)] for x in range(4)}
{0: [0, 1, 2], 1: [1, 2, 3], 2: [2, 3, 4], 3: [3, 4, 5]}
>>>
>>>
>>> {x: [y % 2 for y in range(10)] for x in 'ABC'}
{'A': [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
 'B': [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
 'C': [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]}>>>
>>>
>>> {'ABCDE'[i]: [i % 2]*5 for i in range(5)}
{'A': [0, 0, 0, 0, 0],
 'B': [1, 1, 1, 1, 1],
 'C': [0, 0, 0, 0, 0],
 'D': [1, 1, 1, 1, 1],
 'E': [0, 0, 0, 0, 0]}
>>> {x: {y: 0 for y in 'XYZ'} for x in 'ABC'}
{'A': {'X': 0, 'Y': 0, 'Z': 0},
 'B': {'X': 0, 'Y': 0, 'Z': 0},
 'C': {'X': 0, 'Y': 0, 'Z': 0}}
>>>
>>>
>>> {x: {y: x for y in 'XYZ'} for x in 'ABC'}
{'A': {'X': 'A', 'Y': 'A', 'Z': 'A'},
 'B': {'X': 'B', 'Y': 'B', 'Z': 'B'},
 'C': {'X': 'C', 'Y': 'C', 'Z': 'C'}}

Создаваемые генераторы словари могут иметь и более сложную структуру:

>>> {x: {y: [z for z in range(z, z+ 2)] for y in 'XYZ'} for x, z in zip('ABC', range(3))}
{'A': {'X': [0, 1], 'Y': [0, 1], 'Z': [0, 1]}, 
 'B': {'X': [1, 2], 'Y': [1, 2], 'Z': [1, 2]},
 'C': {'X': [2, 3], 'Y': [2, 3], 'Z': [2, 3]}}

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

>>> {i: {j**2 % i for j in range(1, 100)} for i in [4, 5, 8, 9]}
{4: {0, 1}, 5: {0, 1, 4}, 8: {0, 1, 4}, 9: {0, 1, 4, 7}}

Условие if и if... else...

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

>>> {x: {y: 3 for y in 'ABCD' if y != x} for x in 'ABCD'}
{'A': {'B': 3, 'C': 3, 'D': 3},
 'B': {'A': 3, 'C': 3, 'D': 3},
 'C': {'A': 3, 'B': 3, 'D': 3},
 'D': {'A': 3, 'B': 3, 'C': 3}}

Так мы получили граф из четырех вершин, расстояние между которыми одинаково и равно \(3\).

Условное выражение if... else... указывается перед объявлением цикла for:

>>> {x: 1 if x in 'ACE' else 0 for x in 'ABCDEF'}
{'A': 1, 'B': 0, 'C': 1, 'D': 0, 'E': 1, 'F': 0}

Уловное выражение if... else... может быть применено как для формирования ключей, так и значений элементов:

>>> {'ABC'[z] if z % 2 == 0 else 'XYZ'[z]: z for z in range(3)}
{'A': 0, 'Y': 1, 'C': 2}
>>>
>>> {x: -1 if x % 2 != 1 else 1 for x in range(10)}
{0: -1, 1: 1, 2: -1, 3: 1, 4: -1, 5: 1, 6: -1, 7: 1, 8: -1, 9: 1}

Применение if... else... для одновременного формирования и ключей и значений так же возможно:

>>> {'ABC'[z//2] if z % 2 == 0 else 'XYZ'[z//2 + 1]: -1 if z % 3 == 0 else 1 for z in range(-1, 5)}
{'X': 1, 'A': -1, 'Y': 1, 'B': 1, 'Z': -1, 'C': 1}

Условия if и if... else... могут использоваться совместно:

>>> {x: x**2 if x % 2 == 0 else x**3 for x in range(15) if x % 3 != 1}
{0: 0, 2: 4, 3: 27, 5: 125, 6: 36, 8: 64, 9: 729, 11: 1331, 12: 144, 14: 196}

Использование представлений

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

>>> a
{'A': 0, 'B': 1, 'C': 2, 'D': 3}
>>>
>>> b = dict(zip('BCDE', range(4, 8)))
>>> b
{'B': 4, 'C': 5, 'D': 6, 'E': 7}
>>>
>>> {x: (a[x], b[x]) for x in a.keys() & b.keys()}
{'C': (2, 5), 'D': (3, 6), 'B': (1, 4)}
>>>
>>> {x: a[x] if x in a else b[x] for x in a.keys() ^ b.keys()}
{'E': 7, 'A': 0}
>>>
>>> {x: 1 if x in a.keys() & b.keys() else 0 for x in a.keys() | b.keys()}
{'D': 1, 'E': 0, 'A': 0, 'C': 1, 'B': 1}

Что бы поменять ключи и значения в элементах местами можно воспользоваться представлением элементов словаря:

>>> {v: k for k, v in a.items()}
{0: 'A', 1: 'B', 2: 'C', 3: 'D'}